Skip to content

Commit

Permalink
All: Fixed issue when trying to sync an item associated with a share …
Browse files Browse the repository at this point in the history
…that no longer exists
  • Loading branch information
laurent22 committed Jun 20, 2021
1 parent 112157e commit 5bb68ba
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 35 deletions.
9 changes: 5 additions & 4 deletions packages/lib/JoplinServerApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export default class JoplinServerApi {
return _('Could not connect to Joplin Server. Please check the Synchronisation options in the config screen. Full error was:\n\n%s', msg);
}

private hidePassword(o: any): any {
private hidePasswords(o: any): any {
if (typeof o === 'string') {
try {
const output = JSON.parse(o);
Expand All @@ -104,6 +104,7 @@ export default class JoplinServerApi {
} else {
const output = { ...o };
if (output.password) output.password = '******';
if (output['X-API-AUTH']) output['X-API-AUTH'] = '******';
return output;
}
}
Expand All @@ -114,14 +115,14 @@ export default class JoplinServerApi {
output.push('-v');
if (options.method) output.push(`-X ${options.method}`);
if (options.headers) {
const headers = this.hidePasswords(options.headers);
for (const n in options.headers) {
if (!options.headers.hasOwnProperty(n)) continue;
const headerValue = n === 'X-API-AUTH' ? '******' : options.headers[n];
output.push(`${'-H ' + '"'}${n}: ${headerValue}"`);
output.push(`${'-H ' + '"'}${n}: ${headers[n]}"`);
}
}
if (options.body) {
const serialized = typeof options.body !== 'string' ? JSON.stringify(this.hidePassword(options.body)) : this.hidePassword(options.body);
const serialized = typeof options.body !== 'string' ? JSON.stringify(this.hidePasswords(options.body)) : this.hidePasswords(options.body);
output.push(`${'--data ' + '\''}${serialized}'`);
}
output.push(`'${url}'`);
Expand Down
80 changes: 54 additions & 26 deletions packages/lib/models/Folder.sharing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,37 +326,65 @@ describe('models_Folder.sharing', function() {

await Folder.updateAllShareIds();

// await NoteResource.updateResourceShareIds();

{
const resource: ResourceEntity = await Resource.load(resourceId);
expect(resource.share_id).toBe('');
}
});

// it('should not recursively delete when non-owner deletes a shared folder', async () => {
// const folder = await createFolderTree('', [
// {
// title: 'folder 1',
// children: [
// {
// title: 'note 1',
// },
// ],
// },
// ]);

// BaseItem.shareService_ = {
// isSharedFolderOwner: (_folderId: string) => false,
// } as any;

// await Folder.save({ id: folder.id, share_id: 'abcd1234' });
// await Folder.updateAllShareIds();

// await Folder.delete(folder.id);

// expect((await Folder.all()).length).toBe(0);
// expect((await Note.all()).length).toBe(1);
// });
it('should unshare items that are no longer part of an existing share', async () => {
await createFolderTree('', [
{
title: 'folder 1',
share_id: '1',
children: [
{
title: 'note 1',
},
],
},
{
title: 'folder 2',
share_id: '2',
children: [
{
title: 'note 2',
},
],
},
]);

const resourceService = new ResourceService();

let note1: NoteEntity = await Note.loadByTitle('note 1');
let note2: NoteEntity = await Note.loadByTitle('note 2');
note1 = await shim.attachFileToNote(note1, testImagePath);
note2 = await shim.attachFileToNote(note2, testImagePath);
const resourceId1 = (await Note.linkedResourceIds(note1.body))[0];
const resourceId2 = (await Note.linkedResourceIds(note2.body))[0];

await resourceService.indexNoteResources();

await Folder.updateAllShareIds();

await Folder.updateNoLongerSharedItems(['1']);

// At this point, all items associated with share 2 should have their
// share_id cleared, because the share no longer exists. We also
// double-check that share 1 hasn't been cleared.
expect((await Note.loadByTitle('note 1')).share_id).toBe('1');
expect((await Note.loadByTitle('note 2')).share_id).toBe('');
expect((await Folder.loadByTitle('folder 1')).share_id).toBe('1');
expect((await Folder.loadByTitle('folder 2')).share_id).toBe('');
expect((await Resource.load(resourceId1)).share_id).toBe('1');
expect((await Resource.load(resourceId2)).share_id).toBe('');

// If we pass an empty array, it means there are no active share
// anymore, so all share_id should be cleared.
await Folder.updateNoLongerSharedItems([]);
expect((await Note.loadByTitle('note 1')).share_id).toBe('');
expect((await Folder.loadByTitle('folder 1')).share_id).toBe('');
expect((await Resource.load(resourceId1)).share_id).toBe('');
});

});
32 changes: 32 additions & 0 deletions packages/lib/models/Folder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,38 @@ export default class Folder extends BaseItem {
await this.updateResourceShareIds();
}

// Clear the "share_id" property for the items that are associated with a
// share that no longer exists.
public static async updateNoLongerSharedItems(activeShareIds: string[]) {
const tableNameToClasses: Record<string, any> = {
'folders': Folder,
'notes': Note,
'resources': Resource,
};

for (const tableName of ['folders', 'notes', 'resources']) {
const ItemClass = tableNameToClasses[tableName];

const query = activeShareIds.length ? `
SELECT id FROM ${tableName}
WHERE share_id NOT IN ("${activeShareIds.join('","')}")
` : `
SELECT id FROM ${tableName}
WHERE share_id != ''
`;

const rows = await this.db().selectAll(query);

for (const row of rows) {
await ItemClass.save({
id: row.id,
share_id: '',
updated_time: Date.now(),
}, { autoTimestamp: false });
}
}
}

static async allAsTree(folders: FolderEntity[] = null, options: any = null) {
const all = folders ? folders : await this.all(options);

Expand Down
28 changes: 25 additions & 3 deletions packages/lib/services/share/ShareService.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Store } from 'redux';
import JoplinServerApi from '../../JoplinServerApi';
import Logger from '../../Logger';
import Folder from '../../models/Folder';
import Note from '../../models/Note';
import Setting from '../../models/Setting';
import { State, stateRootKey, StateShare } from './reducer';

const logger = Logger.create('ShareService');

export default class ShareService {

private static instance_: ShareService;
Expand Down Expand Up @@ -152,6 +155,10 @@ export default class ShareService {
return this.shares.filter(s => !!s.note_id).map(s => s.note_id);
}

public get shareInvitations() {
return this.state.shareInvitations;
}

public async addShareRecipient(shareId: string, recipientEmail: string) {
return this.api().exec('POST', `api/shares/${shareId}/users`, {}, {
email: recipientEmail,
Expand Down Expand Up @@ -216,11 +223,26 @@ export default class ShareService {
});
}

private async updateNoLongerSharedItems() {
const shareIds = this.shares.map(share => share.id).concat(this.shareInvitations.map(si => si.share.id));
await Folder.updateNoLongerSharedItems(shareIds);
}

public async maintenance() {
if (this.enabled) {
await this.refreshShareInvitations();
await this.refreshShares();
Setting.setValue('sync.userId', this.api().userId);
let hasError = false;
try {
await this.refreshShareInvitations();
await this.refreshShares();
Setting.setValue('sync.userId', this.api().userId);
} catch (error) {
hasError = true;
logger.error('Failed to run maintenance:', error);
}

// If there was no errors, it means we have all the share objects,
// so we can run the clean up function.
if (!hasError) await this.updateNoLongerSharedItems();
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/lib/shim.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ResourceEntity } from './services/database/types';
import { NoteEntity, ResourceEntity } from './services/database/types';

let isTestingEnv_ = false;

Expand Down Expand Up @@ -211,7 +211,7 @@ const shim = {

detectAndSetLocale: null as Function,

attachFileToNote: async (_note: any, _filePath: string) => {
attachFileToNote: async (_note: any, _filePath: string): Promise<NoteEntity> => {
throw new Error('Not implemented');
},

Expand Down

0 comments on commit 5bb68ba

Please sign in to comment.