Skip to content

Commit

Permalink
Move paths: move paths sdk method with commented out textile call (#57)
Browse files Browse the repository at this point in the history
* Move paths: move paths sdk method with commented out textile call (to be uncommented when txl pkg is released).

* Move: PR comment and await promise array

* Integration test against dev hub and fix bugs

* Fix: update documentation

* Mailbox should use @spacehq/users 0.0.29
  • Loading branch information
jsonsivar authored Mar 11, 2021
1 parent 8d02f86 commit 757d217
Show file tree
Hide file tree
Showing 6 changed files with 400 additions and 292 deletions.
73 changes: 72 additions & 1 deletion integration_tests/storage_interactions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { AddItemsEventData,
ListDirectoryResponse,
DirectoryEntry,
FileNotFoundError,
TxlSubscribeEventData } from '@spacehq/sdk';
TxlSubscribeEventData,
MovePathsResultSummary,
MovePathsEventData } from '@spacehq/sdk';
import { isNode } from 'browser-or-node';
import fs from 'fs';
import { expect, use } from 'chai';
Expand Down Expand Up @@ -277,4 +279,73 @@ describe('Users storing data', () => {
const data = await eventData;
expect(data.bucketName).to.equal('personal');
}).timeout(TestsDefaultTimeout);

it('user should move paths successfully', async () => {
const { user } = await authenticateAnonymousUser();
const txtContent = 'Some manual text should be in the file';

const storage = new UserStorage(user, TestStorageConfig);
const uploadResponse = await storage.addItems({
bucket: 'personal',
files: [
{
path: 'top.txt',
data: txtContent,
mimeType: 'plain/text',
},
{
path: 'subfolder/inner.txt',
data: 'some other stuffs',
mimeType: 'plain/text',
},
],
});

let uploadSummary: AddItemsResultSummary | undefined;
await new Promise((resolve) => {
uploadResponse.once('done', (data: AddItemsEventData) => {
uploadSummary = data as AddItemsResultSummary;
resolve();
});
});

await storage.createFolder({ bucket: 'personal', path: 'moveDestination' });

const moveResponse = await storage.movePaths('personal', [
'top.txt',
'subfolder/inner.txt',
], [
'moveDestination/top.txt',
'moveDestination/inner.txt',
]);

let summary: MovePathsResultSummary | undefined;
await new Promise((resolve) => {
moveResponse.once('done', (data: MovePathsEventData) => {
summary = data as MovePathsResultSummary;
resolve();
});
});

expect(summary?.count).to.equal(2);

// validate files are in the directory
const listFolder = await storage.listDirectory({ bucket: 'personal', path: 'moveDestination' });
expect(listFolder.items).to.containSubset([
{
name: 'top.txt',
isDir: false,
},
{
name: 'inner.txt',
isDir: false,
},
]);

// validate content of top.txt file
const fileResponse = await storage.openFile({ bucket: 'personal', path: '/moveDestination/top.txt' });
const actualTxtContent = await fileResponse.consumeStream();
expect(new TextDecoder('utf8').decode(actualTxtContent)).to.equal(txtContent);
expect(fileResponse.mimeType).to.equal('plain/text');
}).timeout(TestsDefaultTimeout);
});
2 changes: 1 addition & 1 deletion packages/mailbox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
},
"dependencies": {
"@improbable-eng/grpc-web": "^0.14.0",
"@spacehq/users": "^0.0.13",
"@spacehq/users": "^0.0.29",
"@spacehq/utils": "^0.0.29",
"@textile/crypto": "^2.0.0",
"@types/lodash": "^4.14.165",
Expand Down
2 changes: 1 addition & 1 deletion packages/storage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@spacehq/users": "^0.0.29",
"@spacehq/utils": "^0.0.29",
"@textile/crypto": "^2.0.0",
"@textile/hub": "^4.1.0",
"@textile/hub": "^6.1.0",
"@textile/threads-id": "^0.4.0",
"browser-or-node": "^1.3.0",
"cids": "^1.1.4",
Expand Down
25 changes: 25 additions & 0 deletions packages/storage/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,31 @@ export interface AddItemsResponse {
off: (type: AddItemsEventType, listener: AddItemsListener) => void;
}

export interface MovePathsStatus {
sourcePath: string;
destPath: string;
status: 'success' | 'error';
error?: Error;
}

export interface MovePathsResultSummary {
count: number;
}

export type MovePathsEventData = MovePathsStatus | MovePathsResultSummary;
export type MovePathsEventType = 'data' | 'error' | 'done';
export type MovePathsListener = (data: MovePathsEventData) => void;

export interface MovePathsResponse {
on: (type: MovePathsEventType, listener: MovePathsListener) => void;
/**
* this function should only be used to listen for the `'done'` event, since the listener would only be called once.
* or else you could end up having functions leaking (unless you explicitly call the `off()` function).
*/
once: (type: MovePathsEventType, listener: MovePathsListener) => void;
off: (type: MovePathsEventType, listener: MovePathsListener) => void;
}

/**
* SharedWithMeFiles Represents a file created for the user
*
Expand Down
80 changes: 80 additions & 0 deletions packages/storage/src/userStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ import { AcceptInvitationResponse,
ListDirectoryRequest,
ListDirectoryResponse,
MakeFilePublicRequest,
MovePathsResponse,
MovePathsResultSummary,
MovePathsStatus,
Notification,
NotificationSubscribeResponse,
NotificationType,
Expand Down Expand Up @@ -1008,6 +1011,83 @@ export class UserStorage {
return existingStatusbyId;
}

/**
* Moves files in a given bucket from source to destination. Multiple moves can be
* requested by matching up the indices of the sourcePath and destPath arrays
*
* @param bucketName - name of bucket
* @param sourcePaths - array of strings corresponding to the source paths
* @param destPaths - array of strings corresponding to the target paths
*
*/
public async movePaths(bucketName:string, sourcePaths: string[], destPaths: string[]): Promise<MovePathsResponse> {
if (sourcePaths.length !== destPaths.length) {
throw new Error('Source and destination array length mismatch');
}

const client = this.getUserBucketsClient();
const bucket = await this.getOrCreateBucket(client, bucketName);
const emitter = ee();

// using setImmediate here to ensure a cycle is skipped
// giving the caller a chance to listen to emitter in time to not
// miss an early data or error event
setImmediate(() => {
this.moveMultiplePaths(bucketName, sourcePaths, destPaths, client, bucket, emitter).then((summary) => {
emitter.emit('done', summary);
});
});

return emitter;
}

private async moveMultiplePaths(
bucketName:string,
sourcePaths: string[],
destPaths: string[],
client: Buckets,
bucket: BucketMetadataWithThreads,
emitter: ee.Emitter,
): Promise<MovePathsResultSummary> {
const metadataStore = await this.getMetadataStore();
const rootKey = bucket.root?.key || '';
const summary: MovePathsResultSummary = {
count: 0,
};

// eslint-disable-next-line no-restricted-syntax
for (const [index, sourcePath] of sourcePaths.entries()) {
const destPath = destPaths[index];

const status: MovePathsStatus = {
sourcePath,
destPath,
status: 'success',
};

try {
await client.movePath(rootKey, sourcePath, destPath);
summary.count += 1;

const fileMd = await metadataStore.findFileMetadata(bucket.slug, bucket.dbId, `/${sourcePath}`);

if (!fileMd) {
throw new Error('Unable to find file metadata when moving file');
}

fileMd.path = `/${destPaths[index]}`;
const savedFileMd = await metadataStore.upsertFileMetadata(fileMd);
} catch (err) {
status.status = 'error';
status.error = err;
}

emitter.emit('data', status);
}

return summary;
}

/**
* Return the list of shared files accepted by user
*
Expand Down
Loading

0 comments on commit 757d217

Please sign in to comment.