diff --git a/lib/dropbox/account.rb b/lib/dropbox/account.rb index 2476ae4..823673a 100644 --- a/lib/dropbox/account.rb +++ b/lib/dropbox/account.rb @@ -4,7 +4,7 @@ class Account def initialize(attrs={}) @account_id = attrs['account_id'] - @display_name = attrs['name']['display_name'] + @display_name = attrs['name']['display_name'] if attrs['name'] @email = attrs['email'] @email_verified = attrs['email_verified'] @disabled = attrs['disabled'] @@ -44,4 +44,14 @@ def initialize(attrs={}) @allocated = attrs['allocation']['allocated'] # Space allocated in bytes end end + + class UserMembershipInfo + attr_reader :access_type, :account_id, :same_team, :team_member_id + + def initialize(attrs={}) + @access_type = attrs['access_type']['.tag'] + @account_id = attrs['user']['account_id'] + @same_team = attrs['user']['same_team'] + end + end end diff --git a/lib/dropbox/client.rb b/lib/dropbox/client.rb index 2d80c2d..3b4b8b5 100644 --- a/lib/dropbox/client.rb +++ b/lib/dropbox/client.rb @@ -314,6 +314,115 @@ def get_space_usage SpaceUsage.new(resp) end + # List shared folders owned by the current user + # @param limit [Integer] The maximum number of results to return. Defaults to 1000 + # @return [Array] entries + # @return [String] cursor + def list_shared_folders(limit = 1000) + resp = request('/sharing/list_folders', limit: limit) + entries = resp['entries'].map { |entry| FolderMetadata.new(entry) } + [entries, resp['cursor']] + end + + # Continue the list of shared folders + # @param cursor [String] + # @return [Array] entries + # @return [String] cursor + def continue_list_shared_folders(cursor) + resp = request('/sharing/list_folders/continue', cursor: cursor) + entries = resp['entries'].map { |entry| FolderMetadata.new(entry) } + [entries, resp['cursor']] + end + + # Make a folder shared + # @param path [String] The path to the folder to be made shared + # @param member_policy [String] Can be 'anyone' or 'team'. Defaults to 'anyone'. + # @param acl_update_policy [String] Can be 'owner' or 'editors'. Defaults to 'owner'. + # @param shared_link_policy [String] Can be 'anyone' or 'editors'. Defaults to 'anyone'. + # @param force_async [Boolean] Defaults to false. + # @return [String] the job id, if the processing is asynchronous. + # @return [Dropbox::SharedFolderMetadata] if the processing is synchronous + def share_folder(path, member_policy: 'anyone', acl_update_policy: 'owner', shared_link_policy: 'anyone', force_async: false) + resp = request('/sharing/share_folder', path: path, member_policy: member_policy, acl_update_policy: acl_update_policy, shared_link_policy: shared_link_policy, force_async: force_async) + parse_tagged_response(resp) + end + + # Update the sharing policies for a shared folder. + # @params shared_folder_id [String] The shared_id of the folder + # @params member_policy [String]. Can be nil, 'anyone' or 'team'. Defaults to nil + # @params acl_update_policy [String]. Can be nil, 'owner' or 'editors'. Defaults to nil. + # @params shared_link_policy [String]. Can be nil, 'anyone' or 'members'. Defaults to nil. + # @return [Dropbox::SharedFolderMetadata] + def update_folder_policy(shared_folder_id, member_policy: nil, acl_update_policy: nil, shared_link_policy: nil) + data = {shared_folder_id: shared_folder_id} + data[:member_policy] = member_policy if member_policy + data[:acl_update_policy] = acl_update_policy if acl_update_policy + data[:shared_link_policy] = shared_link_policy if shared_link_policy + resp = request('/sharing/update_folder_policy', data) + SharedFolderMetadata.new(resp) + end + + # Add a member to a shared folder + # @param shared_folder_id [String] The shared_id of the folder + # @param members [Array] An array of emails as Strings. + # @param quiet [Boolean] defaults to false + # @param custom_message [String] A custom message to be sent to all of the members. Defaults to nil. + # @param access_level [String] The access level given to all members. Can be 'editor', 'viewer' or 'viewer_no_comment'. Defaults to 'editor'. + # @return [void] + def add_folder_member(shared_folder_id, members, quiet: false, custom_message: nil, access_level: 'editor') + params = {shared_folder_id: shared_folder_id, quiet: quiet} + params[:members] = members.map do |member| + { + 'member' => { + '.tag' => 'email', + 'email' => member + }, + 'access_level' => { + '.tag' => access_level + } + } + end + params[:custom_message] = custom_message if custom_message + + request('/sharing/add_folder_member', params) + nil + end + + # Mount a folder (accept a share request) + # @param shared_folder_id [String] + # @return [Dropbox::SharedFolderMetadata] + def mount_folder(shared_folder_id) + resp = request('/sharing/mount_folder', shared_folder_id: shared_folder_id) + SharedFolderMetadata.new(resp) + end + + # List members of a shared folder + # @param shared_folder_id [String] + # @return [Array] + def list_folder_members(shared_folder_id) + resp = request('/sharing/list_folder_members', shared_folder_id: shared_folder_id) + resp['users'].map {|user| UserMembershipInfo.new(user)} + end + + # Relinquish membership of a shared folder you are a member of + # @param shared_folder_id [String] + # @param leave_a_copy [String] default false + # @return [String] 'complete' if the processing is complete + # @return [String] the job id, if the processing is asynchronous. + def relinquish_folder_membership(shared_folder_id, leave_a_copy: false) + resp = request('/sharing/relinquish_folder_membership', shared_folder_id: shared_folder_id, leave_a_copy: leave_a_copy) + parse_tagged_response(resp) + end + + # Transfer ownership of a shared folder to another member of the folder + # @param shared_folder_id [String] + # @param to_dropbox_id [String] + # @return [void] + def transfer_folder(shared_folder_id, to_dropbox_id) + request('/sharing/transfer_folder', shared_folder_id: shared_folder_id, to_dropbox_id: to_dropbox_id) + nil + end + private def parse_tagged_response(resp) case resp['.tag'] @@ -328,7 +437,13 @@ def parse_tagged_response(resp) when 'full_account' FullAccount.new(resp) when 'complete' - FileMetadata.new(resp) + if resp['time_invited'] + SharedFolderMetadata.new(resp) + elsif resp['client_modified'] + FileMetadata.new(resp) + else + 'complete' + end when 'async_job_id' resp['async_job_id'] when 'in_progress' @@ -347,7 +462,7 @@ def request(action, data=nil) .post(url, json: data) raise ApiError.new(resp) if resp.code != 200 - JSON.parse(resp.to_s) + resp.parse end def content_request(action, args={}) diff --git a/lib/dropbox/metadata.rb b/lib/dropbox/metadata.rb index ac3669f..2a1837d 100644 --- a/lib/dropbox/metadata.rb +++ b/lib/dropbox/metadata.rb @@ -14,7 +14,7 @@ def initialize(attrs={}) # Contains the metadata (but not contents) of a file. class FileMetadata < Metadata - attr_reader :id, :client_modified, :server_modified, :rev, :size + attr_reader :id, :client_modified, :server_modified, :rev, :size, :parent_shared_folder_id def initialize(attrs={}) @id = attrs.delete('id') @@ -24,6 +24,7 @@ def initialize(attrs={}) @server_modified = Time.parse(attrs.delete('server_modified')) @rev = attrs.delete('rev') @size = attrs.delete('size') + @parent_shared_folder_id = attrs.delete('parent_shared_folder_id') super(attrs) end @@ -34,10 +35,12 @@ def ==(cmp) # Contains the metadata (but not contents) of a folder. class FolderMetadata < Metadata - attr_reader :id + attr_reader :id, :shared_folder_id, :parent_shared_folder_id def initialize(attrs={}) @id = attrs.delete('id') + @shared_folder_id = attrs.delete('shared_folder_id') + @parent_shared_folder_id = attrs.delete('parent_shared_folder_id') super(attrs) end @@ -49,4 +52,12 @@ def ==(cmp) # Contains the metadata of a deleted file. class DeletedMetadata < Metadata end + + class SharedFolderMetadata < Metadata + attr_reader :shared_folder_id + def initialize(attrs={}) + @shared_folder_id = attrs.delete('shared_folder_id') + super(attrs) + end + end end diff --git a/test/stubs/complete.json b/test/stubs/complete.json new file mode 100644 index 0000000..d77ed64 --- /dev/null +++ b/test/stubs/complete.json @@ -0,0 +1,6 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + ".tag": "complete" +} diff --git a/test/stubs/list_folder_members.json b/test/stubs/list_folder_members.json new file mode 100644 index 0000000..63d46c9 --- /dev/null +++ b/test/stubs/list_folder_members.json @@ -0,0 +1,11 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "users": + [{"access_type": {".tag": "owner"}, "is_inherited": false, "user": {"account_id": "dbid:user1", "same_team": false}}, + {"access_type": {".tag": "editor"}, "is_inherited": false, "user": {"account_id": "dbid:user2", "same_team": false}}, + {"access_type": {".tag": "editor"}, "is_inherited": false, "user": {"account_id": "dbid:user3", "same_team": false}}], + "groups": [], + "invitees": [] +} diff --git a/test/stubs/list_shared_folders.json b/test/stubs/list_shared_folders.json new file mode 100644 index 0000000..4c1d37e --- /dev/null +++ b/test/stubs/list_shared_folders.json @@ -0,0 +1,23 @@ +HTTP/1.1 200 OK +Content-Type: application/json + + {"entries": + [{"access_type": {".tag": "editor"}, + "is_team_folder": false, + "policy": {"acl_update_policy": {".tag": "editors"}, "shared_link_policy": {".tag": "anyone"}}, + "path_lower": "/shared_file_1", + "name": "shared_file_1", + "shared_folder_id": "1234", + "permissions": [], + "time_invited": "2016-09-12T23:07:11Z", + "preview_url": "https://www.dropbox.com/scl/fo/abcd/1234"}, + {"access_type": {".tag": "owner"}, + "is_team_folder": false, + "policy": {"acl_update_policy": {".tag": "editors"}, "shared_link_policy": {".tag": "anyone"}}, + "path_lower": "/shared_file_2", + "name": "shared_file_2", + "shared_folder_id": "1235", + "permissions": [], + "time_invited": "2016-09-12T22:36:20Z", + "preview_url": "https://www.dropbox.com/scl/fo/abce/1235"}], + "cursor": "cursor123"} diff --git a/test/stubs/null.json b/test/stubs/null.json new file mode 100644 index 0000000..4be915c --- /dev/null +++ b/test/stubs/null.json @@ -0,0 +1,4 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +null diff --git a/test/stubs/share_folder.json b/test/stubs/share_folder.json new file mode 100644 index 0000000..b30ac76 --- /dev/null +++ b/test/stubs/share_folder.json @@ -0,0 +1,13 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + ".tag": "complete", + "access_type": {".tag": "owner"}, + "is_team_folder": false, + "policy": {"acl_update_policy": {".tag": "editors"}, "shared_link_policy": {".tag": "anyone"}}, + "path_lower": "/test/share_me", + "name": "share_me", + "shared_folder_id": "123123123", + "time_invited": "2016-09-13T23:38:32Z", + "preview_url": "https://www.dropbox.com/scl/fo/123123123/AADAASDKJASDASD"} diff --git a/test/test_integration.rb b/test/test_integration.rb index 2d36446..1855544 100644 --- a/test/test_integration.rb +++ b/test/test_integration.rb @@ -5,10 +5,15 @@ def setup WebMock.allow_net_connect! @client = Dropbox::Client.new(ENV['DROPBOX_SDK_ACCESS_TOKEN']) @box = @client.create_folder('/integration_test_container') + if ENV['DROPBOX_SDK_ACCESS_TOKEN_2'] + @client2 = Dropbox::Client.new(ENV['DROPBOX_SDK_ACCESS_TOKEN_2']) + @box2 = @client2.create_folder('/integration_test_container') + end end def teardown @client.delete(@box.path_lower) + @client2.delete(@box2.path_lower) if @client2 WebMock.disable_net_connect! end @@ -96,4 +101,34 @@ def test_upload_session assert_instance_of Dropbox::FileMetadata, file assert_equal 75, file.size end + + def test_sharing + skip('Please provide a second access token with the DROPBOX_SDK_ACCESS_TOKEN_2 environment variable to run the sharing tests') unless @client2 + folder = @client.create_folder(@box.path_lower + '/testing_share') + shared = @client.share_folder(folder.path_lower) + client1_account = @client.get_current_account + client2_account = @client2.get_current_account + + # @client shares a folder with @client2, and client2 accepts the share + @client.add_folder_member(shared.shared_folder_id, [client2_account.email]) + @client2.mount_folder(shared.shared_folder_id) + @client2.move('/testing_share', @box2.path_lower + '/testing_share') + folder_members = @client2.list_folder_members(shared.shared_folder_id) + assert_equal 2, folder_members.length + assert_includes folder_members.map(&:account_id), client2_account.account_id + assert_includes folder_members.map(&:account_id), client1_account.account_id + + # @client2 uploads a file, and @client can see it + uploaded_path = @box.path_lower + '/testing_share/uploaded.txt' + @client2.upload(uploaded_path, 'uploaded') + files = @client.list_folder(@box.path_lower + '/testing_share') + assert_equal files.first.name, 'uploaded.txt' + + # @client transfers ownership to @client2 and then @client + # removes themselves from the folder + @client.transfer_folder(shared.shared_folder_id, client2_account.account_id) + @client.relinquish_folder_membership(shared.shared_folder_id, leave_a_copy: false) + folder_members = @client2.list_folder_members(shared.shared_folder_id) + assert_equal 1, folder_members.length + end end diff --git a/test/test_metadata.rb b/test/test_metadata.rb index 5072fe2..287247b 100644 --- a/test/test_metadata.rb +++ b/test/test_metadata.rb @@ -4,22 +4,32 @@ class DropboxMetadataTest < Minitest::Test def test_folder_initialize folder = Dropbox::FolderMetadata.new('id' => 'id:123', 'name' => 'child', - 'path_lower' => '/parent/middle/child', 'path_display' => '/parent/middle/child') + 'path_lower' => '/parent/middle/child', 'path_display' => '/parent/middle/child', 'shared_folder_id' => '1234', 'parent_shared_folder_id' => 'abcd') assert_equal 'id:123', folder.id assert_equal 'child', folder.name assert_equal '/parent/middle/child', folder.path_lower assert_equal '/parent/middle/child', folder.path_display + assert_equal '1234', folder.shared_folder_id + assert_equal 'abcd', folder.parent_shared_folder_id end def test_file_initialize file = Dropbox::FileMetadata.new('id' => 'id:123', 'name' => 'file', 'path_lower' => '/folder/file', 'path_display' => '/folder/file', - 'size' => 11, 'server_modified' => '2007-07-07T00:00:00Z') + 'size' => 11, 'server_modified' => '2007-07-07T00:00:00Z', 'parent_shared_folder_id' => 'abcd') assert_equal 'id:123', file.id assert_equal 'file', file.name assert_equal '/folder/file', file.path_lower assert_equal '/folder/file', file.path_display assert_equal 11, file.size + assert_equal 'abcd', file.parent_shared_folder_id + end + + def test_shared_folder_initialize + file = Dropbox::SharedFolderMetadata.new('path_lower'=>'/folder/file', 'name'=>'file', 'shared_folder_id'=>'1234') + assert_equal '/folder/file', file.path_lower + assert_equal 'file', file.name + assert_equal '1234', file.shared_folder_id end def test_folder_equality diff --git a/test/test_sharing.rb b/test/test_sharing.rb new file mode 100644 index 0000000..c6ed4e0 --- /dev/null +++ b/test/test_sharing.rb @@ -0,0 +1,70 @@ +require 'test_helper' + +class DropboxSharingTest < Minitest::Test + def setup + @client = Dropbox::Client.new('super-fake-access-token-1234567890000000000000000000000000000000') + end + + def test_list_shared_folders + stub_request(:post, url('sharing/list_folders')).to_return(stub('list_shared_folders')) + folders, cursor = @client.list_shared_folders + folders.each do |folder| + assert_instance_of Dropbox::FolderMetadata, folder + end + assert_equal 'cursor123', cursor + end + + def test_continue_list_shared_folders + stub_request(:post, url('sharing/list_folders/continue')).to_return(stub('list_shared_folders')) + folders, cursor = @client.continue_list_shared_folders('cursor') + folders.each do |folder| + assert_instance_of Dropbox::FolderMetadata, folder + end + assert_equal 'cursor123', cursor + end + + def test_share_folder + stub_request(:post, url('sharing/share_folder')).to_return(stub('share_folder')) + share = @client.share_folder('/test/share_me') + assert_equal '/test/share_me', share.path_lower + end + + def test_add_folder_member + stub_request(:post, url('sharing/add_folder_member')).to_return(stub('null')) + result = @client.add_folder_member('123123', %w(one@example.com two@example.com)) + assert_nil result + end + + def test_mount_folder + stub_request(:post, url('sharing/mount_folder')).to_return(stub('share_folder')) + share = @client.mount_folder('123123') + assert_equal '/test/share_me', share.path_lower + end + + def test_list_folder_members + stub_request(:post, url('sharing/list_folder_members')).to_return(stub('list_folder_members')) + list = @client.list_folder_members('123123') + assert_equal 3, list.length + assert_equal 'dbid:user1', list.first.account_id + assert_equal false, list.first.same_team + assert_equal 'owner', list.first.access_type + end + + def test_transfer_folder + stub_request(:post, url('sharing/transfer_folder')).to_return(stub('null')) + result = @client.transfer_folder('123123123', '123123aaa') + assert_nil result + end + + def test_relinquish_membership + stub_request(:post, url('sharing/relinquish_folder_membership')).to_return(stub('complete')) + result = @client.relinquish_folder_membership('123123') + assert_equal 'complete', result + end + + def test_update_folder_policy + stub_request(:post, url('sharing/update_folder_policy')).to_return(stub('share_folder')) + result = @client.update_folder_policy('123123', acl_update_policy: 'editors') + assert_equal '/test/share_me', result.path_lower + end +end