mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-21 09:38:01 +02:00
All: Allow modifying a resource metadata only when synchronising (#9114)
This commit is contained in:
parent
0069069254
commit
9aed3e04f4
@ -633,6 +633,7 @@ packages/lib/markdownUtils2.test.js
|
||||
packages/lib/markupLanguageUtils.js
|
||||
packages/lib/migrations/42.js
|
||||
packages/lib/models/Alarm.js
|
||||
packages/lib/models/BaseItem.test.js
|
||||
packages/lib/models/BaseItem.js
|
||||
packages/lib/models/Folder.sharing.test.js
|
||||
packages/lib/models/Folder.test.js
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -615,6 +615,7 @@ packages/lib/markdownUtils2.test.js
|
||||
packages/lib/markupLanguageUtils.js
|
||||
packages/lib/migrations/42.js
|
||||
packages/lib/models/Alarm.js
|
||||
packages/lib/models/BaseItem.test.js
|
||||
packages/lib/models/BaseItem.js
|
||||
packages/lib/models/Folder.sharing.test.js
|
||||
packages/lib/models/Folder.test.js
|
||||
|
1
packages/app-cli/tests/support/sample.txt
Normal file
1
packages/app-cli/tests/support/sample.txt
Normal file
@ -0,0 +1 @@
|
||||
just testing
|
1
packages/app-cli/tests/support/sample2.txt
Normal file
1
packages/app-cli/tests/support/sample2.txt
Normal file
@ -0,0 +1 @@
|
||||
just testing 2
|
@ -3,7 +3,7 @@ import shim from './shim';
|
||||
import Database from './database';
|
||||
import migration42 from './services/database/migrations/42';
|
||||
import migration43 from './services/database/migrations/43';
|
||||
// import migration44 from './services/database/migrations/44';
|
||||
import migration44 from './services/database/migrations/44';
|
||||
import { SqlQuery, Migration } from './services/database/types';
|
||||
import addMigrationFile from './services/database/addMigrationFile';
|
||||
|
||||
@ -127,7 +127,7 @@ INSERT INTO version (version) VALUES (1);
|
||||
const migrations: Migration[] = [
|
||||
migration42,
|
||||
migration43,
|
||||
// migration44,
|
||||
migration44,
|
||||
];
|
||||
|
||||
export interface TableField {
|
||||
|
@ -48,8 +48,7 @@ describe('RotatingLogs', () => {
|
||||
try {
|
||||
dir = await createTempDir();
|
||||
await createTestLogFile(dir);
|
||||
await msleep(100);
|
||||
const rotatingLogs: RotatingLogs = new RotatingLogs(dir, 1, 100);
|
||||
const rotatingLogs: RotatingLogs = new RotatingLogs(dir, 1, 5000);
|
||||
await rotatingLogs.cleanActiveLogFile();
|
||||
await rotatingLogs.deleteNonActiveLogFiles();
|
||||
const files = await readdir(dir);
|
||||
|
@ -604,7 +604,7 @@ export default class Synchronizer {
|
||||
} else {
|
||||
// Note: in order to know the real updated_time value, we need to load the content. In theory we could
|
||||
// rely on the file timestamp (in remote.updated_time) but in practice it's not accurate enough and
|
||||
// can lead to conflicts (for example when the file timestamp is slightly ahead of it's real
|
||||
// can lead to conflicts (for example when the file timestamp is slightly ahead of its real
|
||||
// updated_time). updated_time is set and managed by clients so it's always accurate.
|
||||
// Same situation below for updateLocal.
|
||||
//
|
||||
@ -701,7 +701,15 @@ export default class Synchronizer {
|
||||
logger.warn(`Uploading a large resource (resourceId: ${local.id}, size:${resource.size} bytes) which may tie up the sync process.`);
|
||||
}
|
||||
|
||||
await this.apiCall('put', remoteContentPath, null, { path: localResourceContentPath, source: 'file', shareId: resource.share_id });
|
||||
// We skip updating the blob if it hasn't
|
||||
// been modified since the last sync. In
|
||||
// that case, it means the resource metadata
|
||||
// (title, filename, etc.) has been changed,
|
||||
// but not the data blob.
|
||||
const syncItem = await BaseItem.syncItem(syncTargetId, resource.id, { fields: ['sync_time', 'force_sync'] });
|
||||
if (!syncItem || syncItem.sync_time < resource.blob_updated_time || syncItem.force_sync) {
|
||||
await this.apiCall('put', remoteContentPath, null, { path: localResourceContentPath, source: 'file', shareId: resource.share_id });
|
||||
}
|
||||
} catch (error) {
|
||||
if (isCannotSyncError(error)) {
|
||||
await handleCannotSyncItem(ItemClass, syncTargetId, local, error.message);
|
||||
|
@ -1,17 +1,22 @@
|
||||
const { setupDatabaseAndSynchronizer, switchClient } = require('../testing/test-utils.js');
|
||||
const Folder = require('../models/Folder').default;
|
||||
const Note = require('../models/Note').default;
|
||||
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, syncTargetId, synchronizerStart, msleep } from '../testing/test-utils';
|
||||
import BaseItem from './BaseItem';
|
||||
import Folder from './Folder';
|
||||
import Note from './Note';
|
||||
|
||||
describe('models/BaseItem', () => {
|
||||
describe('BaseItem', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllCleanUp();
|
||||
});
|
||||
|
||||
// This is to handle the case where a property is removed from a BaseItem table - in that case files in
|
||||
// the sync target will still have the old property but we don't need it locally.
|
||||
it('should ignore properties that are present in sync file but not in database when serialising', (async () => {
|
||||
it('should ignore properties that are present in sync file but not in database when serialising', async () => {
|
||||
const folder = await Folder.save({ title: 'folder1' });
|
||||
|
||||
let serialized = await Folder.serialize(folder);
|
||||
@ -20,9 +25,9 @@ describe('models/BaseItem', () => {
|
||||
const unserialized = await Folder.unserialize(serialized);
|
||||
|
||||
expect('ignore_me' in unserialized).toBe(false);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should not modify title when unserializing', (async () => {
|
||||
it('should not modify title when unserializing', async () => {
|
||||
const folder1 = await Folder.save({ title: '' });
|
||||
const folder2 = await Folder.save({ title: 'folder1' });
|
||||
|
||||
@ -35,9 +40,9 @@ describe('models/BaseItem', () => {
|
||||
const unserialized2 = await Folder.unserialize(serialized2);
|
||||
|
||||
expect(unserialized2.title).toBe(folder2.title);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should correctly unserialize note timestamps', (async () => {
|
||||
it('should correctly unserialize note timestamps', async () => {
|
||||
const folder = await Folder.save({ title: 'folder' });
|
||||
const note = await Note.save({ title: 'note', parent_id: folder.id });
|
||||
|
||||
@ -48,9 +53,9 @@ describe('models/BaseItem', () => {
|
||||
expect(unserialized.updated_time).toEqual(note.updated_time);
|
||||
expect(unserialized.user_created_time).toEqual(note.user_created_time);
|
||||
expect(unserialized.user_updated_time).toEqual(note.user_updated_time);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should serialize geolocation fields', (async () => {
|
||||
it('should serialize geolocation fields', async () => {
|
||||
const folder = await Folder.save({ title: 'folder' });
|
||||
let note = await Note.save({ title: 'note', parent_id: folder.id });
|
||||
note = await Note.load(note.id);
|
||||
@ -76,9 +81,9 @@ describe('models/BaseItem', () => {
|
||||
expect(unserialized.latitude).toEqual(note.latitude);
|
||||
expect(unserialized.longitude).toEqual(note.longitude);
|
||||
expect(unserialized.altitude).toEqual(note.altitude);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should serialize and unserialize notes', (async () => {
|
||||
it('should serialize and unserialize notes', async () => {
|
||||
const folder = await Folder.save({ title: 'folder' });
|
||||
const note = await Note.save({ title: 'note', parent_id: folder.id });
|
||||
await Note.save({
|
||||
@ -93,9 +98,9 @@ describe('models/BaseItem', () => {
|
||||
const noteAfter = await Note.unserialize(serialized);
|
||||
|
||||
expect(noteAfter).toEqual(noteBefore);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should serialize and unserialize properties that contain new lines', (async () => {
|
||||
it('should serialize and unserialize properties that contain new lines', async () => {
|
||||
const sourceUrl = `
|
||||
https://joplinapp.org/ \\n
|
||||
`;
|
||||
@ -107,9 +112,9 @@ https://joplinapp.org/ \\n
|
||||
const noteAfter = await Note.unserialize(serialized);
|
||||
|
||||
expect(noteAfter).toEqual(noteBefore);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should not serialize the note title and body', (async () => {
|
||||
it('should not serialize the note title and body', async () => {
|
||||
const note = await Note.save({ title: 'my note', body: `one line
|
||||
two line
|
||||
three line \\n no escape` });
|
||||
@ -121,5 +126,27 @@ three line \\n no escape` });
|
||||
one line
|
||||
two line
|
||||
three line \\n no escape`)).toBe(0);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should update item sync item', async () => {
|
||||
const note1 = await Note.save({ });
|
||||
|
||||
const syncTime = async (itemId: string) => {
|
||||
const syncItem = await BaseItem.syncItem(syncTargetId(), itemId, { fields: ['sync_time'] });
|
||||
return syncItem ? syncItem.sync_time : 0;
|
||||
};
|
||||
|
||||
expect(await syncTime(note1.id)).toBe(0);
|
||||
|
||||
await synchronizerStart();
|
||||
|
||||
const newTime = await syncTime(note1.id);
|
||||
expect(newTime).toBeLessThanOrEqual(Date.now());
|
||||
|
||||
// Check that it doesn't change if we sync again
|
||||
await msleep(1);
|
||||
await synchronizerStart();
|
||||
expect(await syncTime(note1.id)).toBe(newTime);
|
||||
});
|
||||
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
import { ModelType, DeleteOptions } from '../BaseModel';
|
||||
import { BaseItemEntity, DeletedItemEntity, NoteEntity } from '../services/database/types';
|
||||
import { BaseItemEntity, DeletedItemEntity, NoteEntity, SyncItemEntity } from '../services/database/types';
|
||||
import Setting from './Setting';
|
||||
import BaseModel from '../BaseModel';
|
||||
import time from '../time';
|
||||
@ -194,6 +194,14 @@ export default class BaseItem extends BaseModel {
|
||||
return output;
|
||||
}
|
||||
|
||||
public static async syncItem(syncTarget: number, itemId: string, options: LoadOptions = null): Promise<SyncItemEntity> {
|
||||
options = {
|
||||
fields: '*',
|
||||
...options,
|
||||
};
|
||||
return await this.db().selectOne(`SELECT ${this.db().escapeFieldsToString(options.fields)} FROM sync_items WHERE sync_target = ? AND item_id = ?`, [syncTarget, itemId]);
|
||||
}
|
||||
|
||||
public static async allSyncItems(syncTarget: number) {
|
||||
const output = await this.db().selectAll('SELECT * FROM sync_items WHERE sync_target = ?', [syncTarget]);
|
||||
return output;
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { supportDir, setupDatabaseAndSynchronizer, switchClient, simulateReadOnlyShareEnv, expectThrow, createTempFile } from '../testing/test-utils';
|
||||
import { supportDir, setupDatabaseAndSynchronizer, switchClient, simulateReadOnlyShareEnv, expectThrow, createTempFile, msleep } from '../testing/test-utils';
|
||||
import Folder from '../models/Folder';
|
||||
import Note from '../models/Note';
|
||||
import Resource from '../models/Resource';
|
||||
import shim from '../shim';
|
||||
import { ErrorCode } from '../errors';
|
||||
import { remove, pathExists } from 'fs-extra';
|
||||
import { ResourceEntity } from '../services/database/types';
|
||||
|
||||
const testImagePath = `${supportDir}/photo.jpg`;
|
||||
|
||||
@ -95,6 +96,39 @@ describe('models/Resource', () => {
|
||||
expect(originalStat.size).toBe(newStat.size);
|
||||
}));
|
||||
|
||||
it('should set the blob_updated_time property if the blob is updated', (async () => {
|
||||
const note = await Note.save({});
|
||||
await shim.attachFileToNote(note, testImagePath);
|
||||
|
||||
const resourceA: ResourceEntity = (await Resource.all())[0];
|
||||
expect(resourceA.updated_time).toBe(resourceA.blob_updated_time);
|
||||
|
||||
await msleep(1);
|
||||
|
||||
await Resource.updateResourceBlobContent(resourceA.id, testImagePath);
|
||||
|
||||
const resourceB: ResourceEntity = (await Resource.all())[0];
|
||||
expect(resourceB.updated_time).toBeGreaterThan(resourceA.updated_time);
|
||||
expect(resourceB.blob_updated_time).toBeGreaterThan(resourceA.blob_updated_time);
|
||||
}));
|
||||
|
||||
it('should NOT set the blob_updated_time property if the blob is NOT updated', (async () => {
|
||||
const note = await Note.save({});
|
||||
await shim.attachFileToNote(note, testImagePath);
|
||||
|
||||
const resourceA: ResourceEntity = (await Resource.all())[0];
|
||||
|
||||
await msleep(1);
|
||||
|
||||
// We only update the resource metadata - so the blob timestamp should
|
||||
// not change
|
||||
await Resource.save({ id: resourceA.id, title: 'new title' });
|
||||
|
||||
const resourceB: ResourceEntity = (await Resource.all())[0];
|
||||
expect(resourceB.updated_time).toBeGreaterThan(resourceA.updated_time);
|
||||
expect(resourceB.blob_updated_time).toBe(resourceA.blob_updated_time);
|
||||
}));
|
||||
|
||||
it('should not allow modifying a read-only resource', async () => {
|
||||
const { cleanup, resource } = await setupFolderNoteResourceReadOnly('123456789');
|
||||
await expectThrow(async () => Resource.save({ id: resource.id, share_id: '123456789', title: 'cannot do this!' }), ErrorCode.IsReadOnly);
|
||||
|
@ -15,6 +15,7 @@ import JoplinError from '../JoplinError';
|
||||
import itemCanBeEncrypted from './utils/itemCanBeEncrypted';
|
||||
import { getEncryptionEnabled } from '../services/synchronizer/syncInfoUtils';
|
||||
import ShareService from '../services/share/ShareService';
|
||||
import { SaveOptions } from './utils/types';
|
||||
|
||||
export default class Resource extends BaseItem {
|
||||
|
||||
@ -372,9 +373,15 @@ export default class Resource extends BaseItem {
|
||||
// We first save the resource metadata because this can throw, for
|
||||
// example if modifying a resource that is read-only
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
const result = await Resource.save({
|
||||
id: resource.id,
|
||||
size: fileStat.size,
|
||||
updated_time: now,
|
||||
blob_updated_time: now,
|
||||
}, {
|
||||
autoTimestamp: false,
|
||||
});
|
||||
|
||||
// If the above call has succeeded, we save the data blob
|
||||
@ -442,10 +449,18 @@ export default class Resource extends BaseItem {
|
||||
}, { changeSource: ItemChange.SOURCE_SYNC });
|
||||
}
|
||||
|
||||
// public static async save(o: ResourceEntity, options: SaveOptions = null): Promise<ResourceEntity> {
|
||||
// const resource:ResourceEntity = await super.save(o, options);
|
||||
// if (resource.updated_time) resource.bl
|
||||
// return resource;
|
||||
// }
|
||||
public static async save(o: ResourceEntity, options: SaveOptions = null): Promise<ResourceEntity> {
|
||||
const resource = { ...o };
|
||||
|
||||
if (this.isNew(o, options)) {
|
||||
const now = Date.now();
|
||||
options = { ...options, autoTimestamp: false };
|
||||
if (!resource.created_time) resource.created_time = now;
|
||||
if (!resource.updated_time) resource.updated_time = now;
|
||||
if (!resource.blob_updated_time) resource.blob_updated_time = now;
|
||||
}
|
||||
|
||||
return await super.save(resource, options);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -256,6 +256,7 @@ export interface ResourceLocalStateEntity {
|
||||
'type_'?: number;
|
||||
}
|
||||
export interface ResourceEntity {
|
||||
'blob_updated_time'?: number;
|
||||
'created_time'?: number;
|
||||
'encryption_applied'?: number;
|
||||
'encryption_blob_encrypted'?: number;
|
||||
@ -467,6 +468,7 @@ export const databaseSchema: DatabaseTables = {
|
||||
type_: { type: 'number' },
|
||||
},
|
||||
resources: {
|
||||
blob_updated_time: { type: 'number' },
|
||||
created_time: { type: 'number' },
|
||||
encryption_applied: { type: 'number' },
|
||||
encryption_blob_encrypted: { type: 'number' },
|
||||
|
@ -1,9 +1,9 @@
|
||||
import time from '../../time';
|
||||
import shim from '../../shim';
|
||||
import Setting from '../../models/Setting';
|
||||
import { NoteEntity } from '../../services/database/types';
|
||||
import { NoteEntity, ResourceEntity } from '../../services/database/types';
|
||||
import { remoteNotesFoldersResources, remoteResources } from '../../testing/test-utils-synchronizer';
|
||||
import { synchronizerStart, tempFilePath, resourceFetcher, supportDir, setupDatabaseAndSynchronizer, synchronizer, fileApi, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, checkThrowAsync } from '../../testing/test-utils';
|
||||
import { synchronizerStart, tempFilePath, resourceFetcher, supportDir, setupDatabaseAndSynchronizer, synchronizer, fileApi, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, checkThrowAsync, msleep } from '../../testing/test-utils';
|
||||
import Folder from '../../models/Folder';
|
||||
import Note from '../../models/Note';
|
||||
import Resource from '../../models/Resource';
|
||||
@ -27,7 +27,7 @@ describe('Synchronizer.resources', () => {
|
||||
insideBeforeEach = false;
|
||||
});
|
||||
|
||||
it('should sync resources', (async () => {
|
||||
it('should sync resources', async () => {
|
||||
while (insideBeforeEach) await time.msleep(500);
|
||||
|
||||
const folder1 = await Folder.save({ title: 'folder1' });
|
||||
@ -58,9 +58,9 @@ describe('Synchronizer.resources', () => {
|
||||
|
||||
const resourcePath1_2 = Resource.fullPath(resource1_2);
|
||||
expect(fileContentEqual(resourcePath1, resourcePath1_2)).toBe(true);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should handle resource download errors', (async () => {
|
||||
it('should handle resource download errors', async () => {
|
||||
while (insideBeforeEach) await time.msleep(500);
|
||||
|
||||
const folder1 = await Folder.save({ title: 'folder1' });
|
||||
@ -87,9 +87,9 @@ describe('Synchronizer.resources', () => {
|
||||
const ls = await Resource.localState(resource1);
|
||||
expect(ls.fetch_status).toBe(Resource.FETCH_STATUS_ERROR);
|
||||
expect(ls.fetch_error).toBe('did not work');
|
||||
}));
|
||||
});
|
||||
|
||||
it('should set the resource file size if it is missing', (async () => {
|
||||
it('should set the resource file size if it is missing', async () => {
|
||||
while (insideBeforeEach) await time.msleep(500);
|
||||
|
||||
const folder1 = await Folder.save({ title: 'folder1' });
|
||||
@ -110,9 +110,9 @@ describe('Synchronizer.resources', () => {
|
||||
await fetcher.waitForAllFinished();
|
||||
r1 = await Resource.load(r1.id);
|
||||
expect(r1.size).toBe(2720);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should delete resources', (async () => {
|
||||
it('should delete resources', async () => {
|
||||
while (insideBeforeEach) await time.msleep(500);
|
||||
|
||||
const folder1 = await Folder.save({ title: 'folder1' });
|
||||
@ -142,9 +142,9 @@ describe('Synchronizer.resources', () => {
|
||||
allResources = await Resource.all();
|
||||
expect(allResources.length).toBe(0);
|
||||
expect(await shim.fsDriver().exists(resourcePath1)).toBe(false);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should encrypt resources', (async () => {
|
||||
it('should encrypt resources', async () => {
|
||||
setEncryptionEnabled(true);
|
||||
const masterKey = await loadEncryptionMasterKey();
|
||||
|
||||
@ -170,9 +170,9 @@ describe('Synchronizer.resources', () => {
|
||||
const resourcePath1_2 = Resource.fullPath(resource1_2);
|
||||
|
||||
expect(fileContentEqual(resourcePath1, resourcePath1_2)).toBe(true);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should sync resource blob changes', (async () => {
|
||||
it('should sync resource blob changes', async () => {
|
||||
const tempFile = tempFilePath('txt');
|
||||
await shim.fsDriver().writeFile(tempFile, '1234', 'utf8');
|
||||
const folder1 = await Folder.save({ title: 'folder1' });
|
||||
@ -204,9 +204,9 @@ describe('Synchronizer.resources', () => {
|
||||
const resource1_1 = (await Resource.all())[0];
|
||||
expect(resource1_1.size).toBe(newSize);
|
||||
expect(await Resource.resourceBlobContent(resource1_1.id, 'utf8')).toBe('1234 MOD');
|
||||
}));
|
||||
});
|
||||
|
||||
it('should handle resource conflicts', (async () => {
|
||||
it('should handle resource conflicts', async () => {
|
||||
{
|
||||
const tempFile = tempFilePath('txt');
|
||||
await shim.fsDriver().writeFile(tempFile, '1234', 'utf8');
|
||||
@ -271,9 +271,9 @@ describe('Synchronizer.resources', () => {
|
||||
expect(resourceConflictFolder).toBeTruthy();
|
||||
expect(resourceConflictFolder.parent_id).toBeFalsy();
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
it('should handle resource conflicts if a resource is changed locally but deleted remotely', (async () => {
|
||||
it('should handle resource conflicts if a resource is changed locally but deleted remotely', async () => {
|
||||
{
|
||||
const tempFile = tempFilePath('txt');
|
||||
await shim.fsDriver().writeFile(tempFile, '1234', 'utf8');
|
||||
@ -316,9 +316,9 @@ describe('Synchronizer.resources', () => {
|
||||
expect(originalResource.id).not.toBe(conflictResource.id);
|
||||
expect(conflictResource.title).toBe('modified resource');
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
it('should not upload a resource if it has not been fetched yet', (async () => {
|
||||
it('should not upload a resource if it has not been fetched yet', async () => {
|
||||
// In some rare cases, the synchronizer might try to upload a resource even though it
|
||||
// doesn't have the resource file. It can happen in this situation:
|
||||
// - C1 create resource
|
||||
@ -350,9 +350,9 @@ describe('Synchronizer.resources', () => {
|
||||
await BaseItem.saveSyncEnabled(ModelType.Resource, resource.id);
|
||||
await synchronizerStart();
|
||||
expect((await remoteResources()).length).toBe(1);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should not download resources over the limit', (async () => {
|
||||
it('should not download resources over the limit', async () => {
|
||||
const note1 = await Note.save({ title: 'note' });
|
||||
await shim.attachFileToNote(note1, `${supportDir}/photo.jpg`);
|
||||
await synchronizer().start();
|
||||
@ -368,6 +368,53 @@ describe('Synchronizer.resources', () => {
|
||||
expect(syncItems.length).toBe(2);
|
||||
expect(syncItems[1].item_location).toBe(BaseItem.SYNC_ITEM_LOCATION_REMOTE);
|
||||
expect(syncItems[1].sync_disabled).toBe(1);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should not upload blob if it has not changed', async () => {
|
||||
const note = await Note.save({});
|
||||
await shim.attachFileToNote(note, `${supportDir}/sample.txt`);
|
||||
const resource: ResourceEntity = (await Resource.all())[0];
|
||||
const resourcePath = `.resource/${resource.id}`;
|
||||
|
||||
await synchronizer().api().put(resourcePath, 'before upload');
|
||||
expect(await synchronizer().api().get(resourcePath)).toBe('before upload');
|
||||
await synchronizerStart();
|
||||
expect(await synchronizer().api().get(resourcePath)).toBe('just testing');
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Change metadata only and check that blob is not uploaded. To do this,
|
||||
// we manually overwrite the data on the sync target, then sync. If the
|
||||
// synchronizer doesn't upload the blob, this manually changed data
|
||||
// should remain.
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
await Resource.save({ id: resource.id, title: 'my new title' });
|
||||
await synchronizer().api().put(resourcePath, 'check if changed');
|
||||
await synchronizerStart();
|
||||
expect(await synchronizer().api().get(resourcePath)).toBe('check if changed');
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Now change the blob, and check that the remote item has been
|
||||
// overwritten.
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
await Resource.updateResourceBlobContent(resource.id, `${supportDir}/sample.txt`);
|
||||
await synchronizerStart();
|
||||
expect(await synchronizer().api().get(resourcePath)).toBe('just testing');
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Change the blob, then change the metadata, and sync. Even though
|
||||
// blob_updated_time is earlier than updated_time, it should still
|
||||
// update everything on the sync target, because both times are after
|
||||
// the item sync_time.
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
await Resource.updateResourceBlobContent(resource.id, `${supportDir}/sample2.txt`);
|
||||
await msleep(1);
|
||||
await Resource.save({ id: resource.id, title: 'my new title 2' });
|
||||
await synchronizerStart();
|
||||
expect(await synchronizer().api().get(resourcePath)).toBe('just testing 2');
|
||||
expect(await synchronizer().api().get(`${resource.id}.md`)).toContain('my new title 2');
|
||||
});
|
||||
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user