Compare commits

...

7 Commits

Author SHA1 Message Date
Nemo b887268730 update changelog 2024-03-07 13:35:00 +05:30
Nemo 8227e9416f new release 2024-03-07 12:47:33 +05:30
Nemo 9d889db503 update tests 2024-03-07 07:14:51 +00:00
Nemo 239a21948c dependency updates 2024-03-07 06:43:23 +00:00
dhx fd3826a622 Fixes the Docker build
Updated the base image to ruby:3.3-alpine3.19, install alpine-sdk to
be able to build all dependencies
2024-03-07 06:40:08 +00:00
Nemo 787e838a59 Handle redirects properly.
Fixes redirect issues.

httparty sends request body at 302 redirects, so we
disable redirect follows for our fileoperations.redirect
call.

minor changes in client as well:

1. drops the token from the body, and sends it in header instead
   as per the new api
2. support additional options
2024-03-07 06:38:05 +00:00
dhx 831e173170 Fixes the exportAll -> export_all outline API change
As until now the client implementation replaced single underscores with a dot
this was not compatible with underscores in the API URIs.
For this reason now double underscores in the method name are replaced with a
dot instead.
2024-03-07 06:38:05 +00:00
11 changed files with 140 additions and 40 deletions

View File

@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## 1.0.2 - 2024-03-07
### Changed
- Dependency updates
- Fixes for new Outline API
## 1.0.1 - 2020-04-26
### Changed

View File

@ -1,12 +1,18 @@
FROM ruby:2.7-alpine
FROM ruby:3.3-alpine3.19
RUN apk add --no-cache git openssh-client rsync && \
echo -e "StrictHostKeyChecking no" >> /etc/ssh/ssh_config && \
mkdir /root/.ssh
WORKDIR /outliner
COPY . /outliner/
RUN gem install bundler && \
RUN echo "gem: --no-ri --no-rdoc" > ~/.gemrc && \
apk add --no-cache alpine-sdk && \
gem update --system && \
gem install bundler && \
bundle install && \
apk add --no-cache git openssh-client rsync && \
echo -e "StrictHostKeyChecking no" >> /etc/ssh/ssh_config && \
mkdir /root/.ssh
apk del --no-cache alpine-sdk && \
rm ~/.gemrc
ENTRYPOINT ["/outliner/entrypoint.sh"]

View File

@ -28,6 +28,13 @@ require 'outliner'
client = Outliner.new('https://knowledge.example.com')
pp client.auth_info
pp client.collections_list(offset: 0, limit: 10)
# This works around a 302 redirect bug in httparty
begin
r = @client.fileOperations__redirect({id: FILE_OPERATION_ID}, format: nil, no_follow: true)
rescue HTTParty::RedirectionTooDeep => e
# Download this using response = HTTParty.get e.response.header['location'] if needed
pp e.response.header['location']
end
```
### Import

View File

@ -20,13 +20,30 @@ local_directory = ARGV[0]
CLIENT = Outliner::Client.new ENV['OUTLINE_BASE_URI']
# Download the complete zip
response = CLIENT.collections_exportAll(download: true)
response = CLIENT.collections__export_all(format: "outline-markdown")
# Extract it to a tempfle
file = Tempfile.new('download.zip')
File.open(file.path, 'w') { |f| f.write(response.body) }
raise 'Failed to trigger export_all action' if not response['success']
file_operation_id = response['data']['fileOperation']['id']
fop_info_response = nil
i = 0
loop do
i += 1
raise 'Timed out waiting for the file export operation to complete' if i > 20
sleep(2*i)
fop_info_response = CLIENT.fileOperations__info(id: file_operation_id)
raise 'Failed to query export file operation info' if not fop_info_response['ok']
break if fop_info_response['data']['state'] == 'complete'
end
begin
fop_redirect_response = CLIENT.fileOperations__redirect({id: file_operation_id}, {no_follow: true})
rescue HTTParty::RedirectionTooDeep => e
response = HTTParty.get e.response.header['location']
file = Tempfile.new('download.zip')
File.open(file.path, 'w') { |f| f.write(response.body) }
`unzip -o "#{file.path}" -d "#{local_directory}"`
file.unlink
end
`unzip -o "#{file.path}" -d "#{local_directory}"`
# Delete tempfile
file.unlink

View File

@ -28,7 +28,7 @@ def create_documents_recursively(directory, collection_id, parent_document_id =
}
params[:parentDocumentId] = parent_document_id if parent_document_id
CLIENT.documents_create(params)
CLIENT.documents__create(params)
puts "[-] #{file}"
end
@ -42,7 +42,7 @@ def create_documents_recursively(directory, collection_id, parent_document_id =
publish: true,
parentDocumentId: parent_document_id
}
response = CLIENT.documents_create(params)
response = CLIENT.documents__create(params)
create_documents_recursively(dir, collection_id, response['data']['id'])
end
Dir.chdir cwd
@ -65,7 +65,7 @@ begin
rescue StandardError? => e
# If we fail, print an error, and delete the collection
puts "[E] Import failed with error: #{e.message}"
CLIENT.collections_delete(id: root_collection_id)
CLIENT.collections__delete(id: root_collection_id)
puts '[E] Deleted collection, please report the issue or retry'
exit 1
end

View File

@ -11,27 +11,28 @@ module Outliner
end
def find_or_create_collection(name)
collections = self.collections_list(limit: 100)['data']
collections = self.collections__list(limit: 100)['data']
collections.filter!{|c|c['name'] == name}
if collections.size >= 1
collections[0]['id']
else
self.collections_create(name: name, description: 'Imported Collection')['data']['id']
self.collections__create(name: name, description: 'Imported Collection')['data']['id']
end
end
def method_missing(method_name, params = {})
method_name = '/' + method_name.to_s.sub('_', '.')
body = {token: @token}.merge(params).to_json
def method_missing(method_name, params = {}, options = {})
method_name = "/#{method_name.to_s.sub('__', '.')}"
options = {
body: body,
body: params.to_json,
headers: {
'Accept'=>'application/json',
'Content-Type': 'application/json',
'User-Agent': "Outliner/#{Outliner::VERSION}"
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'User-Agent' => "Outliner/#{Outliner::VERSION}",
'Authorization' => "Bearer #{@token}"
},
format: :json
}
format: :json,
}.merge!(options)
self.class.post(method_name, options)
end

View File

@ -1,3 +1,3 @@
module Outliner
VERSION = "1.0.1"
VERSION = "1.0.2"
end

View File

@ -23,10 +23,11 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
spec.add_dependency "httparty", "~> 0.17"
spec.add_dependency "httparty", "~> 0.21"
spec.add_dependency "json", "~> 2.7"
spec.add_development_dependency "bundler", "~> 2.0"
spec.add_dependency "rake", ">= 12.3"
spec.add_development_dependency "webmock", "~> 3.6"
spec.add_development_dependency "minitest", "~> 5.8"
spec.add_development_dependency "bundler", "~> 2.1"
spec.add_dependency "rake", ">= 13.1"
spec.add_development_dependency "webmock", "~> 3.23"
spec.add_development_dependency "minitest", "~> 5.22"
end

View File

@ -0,0 +1,31 @@
{
"success": true,
"data": {
"fileOperation": {
"id": "08d5db26-bf43-4ec9-ac62-8769fd828e94",
"type": "export",
"format": "outline-markdown",
"name": "Acme-export.zip",
"state": "creating",
"error": null,
"size": "0",
"collectionId": null,
"user": {
"id": "817fb131-4a9b-4981-9002-38c2503adc3e",
"name": "Acme Admin",
"avatarUrl": "https://fake-avatar-url.com",
"color": "#2BC2FF",
"isAdmin": true,
"isSuspended": false,
"isViewer": false,
"createdAt": "2024-03-07T04:03:45.204Z",
"updatedAt": "2024-03-07T06:51:26.023Z",
"lastActiveAt": "2024-03-07T06:51:26.023Z"
},
"createdAt": "2024-03-07T06:51:26.031Z",
"updatedAt": "2024-03-07T06:51:26.031Z"
}
},
"status": 200,
"ok": true
}

View File

@ -0,0 +1 @@
Redirecting to https://fake.s3-accelerate.amazonaws.com/uploads/3e11b7f9-f1c0-44d0-a21b-4d6e0561e9c9/a5b6985a-cff6-4d03-be60-20c517bee63e/Acme-export.zip?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=XXXXXXXXXXXXXXXXXXXX%2F20240307%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20240307T055812Z&X-Amz-Expires=60&X-Amz-Signature=ff759b27ddfd5c7401c1715411a8ceba886f9f462c9b52fc0c4a5906e99ecd22&X-Amz-SignedHeaders=host&response-content-disposition=attachment.

View File

@ -6,6 +6,7 @@ require 'json'
class ClientTest < Minitest::Test
TOKEN = "c4302eFAKE_TOKEN9b6e27bccb7"
BASE_URI='https://kb.example.com'
FILE_OPERATION_ID = "08d5db26-bf43-4ec9-ac62-8769fd828e94"
def setup
ENV['OUTLINE_TOKEN'] = TOKEN
@client = Outliner::Client.new BASE_URI
@ -16,9 +17,36 @@ class ClientTest < Minitest::Test
end
def test_auth_info_api
mock('auth.info', 'auth.info.200')
auth_info = @client.auth_info
assert_equal "https://kb.example.com", auth_info['data']['team']['url']
mock('auth.info')
r = @client.auth__info
assert_equal "https://kb.example.com", r['data']['team']['url']
end
def test_export
mock('collections.export_all')
r = @client.collections__export_all
assert_equal FILE_OPERATION_ID, r['data']['fileOperation']['id']
assert_equal 200, r['status']
assert_equal true, r['ok']
end
def test_retrieve_file_operation
mock("fileOperations.redirect", {
id: FILE_OPERATION_ID
}, {
"X-Download-Options" => "noopen",
"X-Content-Type-Options" => "nosniff",
"Content-Type" => "text/plain; charset=utf-8",
"Content-Length" => "459",
"Location" => "https://s3.example.com/#{FILE_OPERATION_ID}"
}, 302)
begin
r = @client.fileOperations__redirect({id: FILE_OPERATION_ID}, format: nil, no_follow: true)
rescue HTTParty::RedirectionTooDeep => e
assert_equal "302", e.response.code
assert_equal "https://s3.example.com/#{FILE_OPERATION_ID}", e.response.header['location']
end
end
private
@ -27,15 +55,16 @@ class ClientTest < Minitest::Test
File.read "test/fixtures/#{file}.json"
end
def mock(method_name, fixture_file, params = {})
def mock(method_name, params = {}, response_headers = {}, status = 200)
stub_request(:post, BASE_URI + "/api/" + method_name)
.with(
body: params.merge({token: TOKEN}).to_json,
body: params.to_json,
headers: {
'Accept'=>'application/json',
'User-Agent'=>"Outliner/#{Outliner::VERSION}",
'Content-Type'=> 'application/json'
'Content-Type'=> 'application/json',
"Authorization"=> "Bearer #{TOKEN}"
}
).to_return(body: read_fixture(fixture_file))
).to_return(body: read_fixture(method_name + ".#{status}"), headers: response_headers, status: 302)
end
end
end