orgnote-api 0.20.2 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api.d.ts +93 -98
- package/constants/{command-groups.contant.d.ts → command-groups.d.ts} +2 -1
- package/constants/{command-groups.contant.js → command-groups.js} +2 -0
- package/constants/extension-errors.d.ts +9 -0
- package/constants/extension-errors.js +18 -0
- package/constants/file-guard-errors.d.ts +8 -0
- package/constants/file-guard-errors.js +18 -0
- package/constants/git-errors.d.ts +9 -0
- package/constants/git-errors.js +18 -0
- package/constants/i18n-keys.d.ts +371 -0
- package/constants/i18n-keys.js +158 -0
- package/constants/index.d.ts +9 -1
- package/constants/index.js +9 -1
- package/constants/oauth-providers.d.ts +1 -0
- package/constants/oauth-providers.js +1 -0
- package/constants/route-names.d.ts +36 -0
- package/constants/route-names.js +37 -0
- package/constants/route-paths.d.ts +5 -0
- package/constants/route-paths.js +4 -0
- package/constants/style-sizes.d.ts +1 -0
- package/constants/style-sizes.js +1 -0
- package/encryption/__tests__/encryption.spec.js +4 -5
- package/encryption/__tests__/note-encryption.spec.js +46 -348
- package/encryption/encryption.d.ts +9 -4
- package/encryption/encryption.js +25 -5
- package/encryption/note-encryption.d.ts +1 -1
- package/encryption/note-encryption.js +6 -6
- package/files-api.d.ts +0 -1
- package/index.d.ts +4 -1
- package/index.js +4 -1
- package/mappers/orgnode-to-note.d.ts +2 -2
- package/mappers/orgnode-to-note.js +3 -2
- package/models/auth-store.d.ts +3 -3
- package/models/buffer-store.d.ts +14 -0
- package/models/buffer.d.ts +24 -0
- package/models/colors.d.ts +1 -0
- package/models/command.d.ts +13 -8
- package/models/commands-group-store.d.ts +11 -0
- package/models/commands-store.d.ts +13 -0
- package/models/completion-store.d.ts +14 -0
- package/models/completion.d.ts +16 -7
- package/models/config-store.d.ts +9 -0
- package/models/confirmation-modal.d.ts +11 -0
- package/models/context-menu-store.d.ts +10 -0
- package/models/cron-store.d.ts +21 -0
- package/models/cron-task.d.ts +30 -0
- package/models/css-utils.d.ts +17 -0
- package/models/default-commands.d.ts +52 -3
- package/models/default-commands.js +59 -2
- package/models/encryption-store.d.ts +10 -0
- package/models/encryption-store.js +1 -0
- package/models/encryption.d.ts +54 -11
- package/models/encryption.js +28 -1
- package/models/extension-registry-store.d.ts +9 -0
- package/models/extension-registry-store.js +1 -0
- package/models/extension-store.d.ts +18 -0
- package/models/extension-store.js +1 -0
- package/models/extension.d.ts +96 -24
- package/models/extension.js +88 -1
- package/models/file-guard-store.d.ts +14 -0
- package/models/file-guard-store.js +1 -0
- package/models/file-guard.d.ts +27 -0
- package/models/file-guard.js +1 -0
- package/models/{file-cache.d.ts → file-info.d.ts} +1 -1
- package/models/file-info.js +1 -0
- package/models/file-manager-store.d.ts +10 -12
- package/models/file-opener-store.d.ts +6 -4
- package/models/file-system-manager-store.d.ts +13 -0
- package/models/file-system-manager-store.js +1 -0
- package/models/file-system-store.d.ts +19 -0
- package/models/file-system-store.js +1 -0
- package/models/file-system.d.ts +32 -5
- package/models/file-system.js +2 -0
- package/models/file-upload.d.ts +6 -0
- package/models/file-upload.js +1 -0
- package/models/file-watcher-store.d.ts +18 -0
- package/models/file-watcher-store.js +1 -0
- package/models/files-store.d.ts +2 -2
- package/models/git-store.d.ts +12 -0
- package/models/git-store.js +1 -0
- package/models/git.d.ts +47 -0
- package/models/git.js +1 -0
- package/models/i18n-keys.d.ts +2 -0
- package/models/i18n-keys.js +1 -0
- package/models/index.d.ts +56 -2
- package/models/index.js +59 -2
- package/models/layout-snapshot-repository.d.ts +14 -0
- package/models/layout-snapshot-repository.js +1 -0
- package/models/layout-store.d.ts +17 -0
- package/models/layout-store.js +1 -0
- package/models/layout.d.ts +16 -0
- package/models/layout.js +1 -0
- package/models/log-repository.d.ts +17 -0
- package/models/log-repository.js +1 -0
- package/models/log-store.d.ts +15 -0
- package/models/log-store.js +1 -0
- package/models/log.d.ts +12 -0
- package/models/log.js +1 -0
- package/models/logger.d.ts +8 -0
- package/models/logger.js +1 -0
- package/models/menu-action.d.ts +18 -0
- package/models/menu-action.js +1 -0
- package/models/modal-store.d.ts +16 -0
- package/models/modal-store.js +1 -0
- package/models/modal.d.ts +10 -2
- package/models/note.d.ts +30 -13
- package/models/notification-config.d.ts +14 -0
- package/models/notification-config.js +1 -0
- package/models/notifications-store.d.ts +20 -0
- package/models/notifications-store.js +1 -0
- package/models/oauth-provider.d.ts +2 -1
- package/models/orgnote-config.d.ts +80 -0
- package/models/orgnote-config.js +42 -0
- package/models/orgnote-url.d.ts +6 -0
- package/models/orgnote-url.js +1 -0
- package/models/pane-snapshot-repository.d.ts +0 -0
- package/models/pane-snapshot-repository.js +0 -0
- package/models/pane.d.ts +38 -0
- package/models/pane.js +1 -0
- package/models/panes-store.d.ts +30 -0
- package/models/panes-store.js +1 -0
- package/models/platform-detection.d.ts +11 -0
- package/models/platform-detection.js +1 -0
- package/models/platform-specific.d.ts +1 -0
- package/models/platform-specific.js +1 -0
- package/models/platform.d.ts +2 -0
- package/models/platform.js +1 -0
- package/models/queue-store.d.ts +49 -0
- package/models/queue-store.js +1 -0
- package/models/queue-task.d.ts +15 -0
- package/models/queue-task.js +1 -0
- package/models/repositories.d.ts +58 -38
- package/models/screen-detection.d.ts +10 -0
- package/models/screen-detection.js +1 -0
- package/models/settings-store.d.ts +12 -0
- package/models/settings-store.js +1 -0
- package/models/settings-ui-store.d.ts +7 -0
- package/models/settings-ui-store.js +1 -0
- package/models/sidebar-store.d.ts +22 -0
- package/models/sidebar-store.js +1 -0
- package/models/splash-screen.d.ts +13 -0
- package/models/splash-screen.js +1 -0
- package/models/store.d.ts +1 -1
- package/models/style-size.d.ts +2 -0
- package/models/style-size.js +1 -0
- package/models/style-variant.d.ts +1 -0
- package/models/style-variant.js +1 -0
- package/models/sync-store.d.ts +8 -6
- package/models/sync.d.ts +0 -5
- package/models/system-info.d.ts +47 -0
- package/models/system-info.js +1 -0
- package/models/theme-store.d.ts +16 -0
- package/models/theme-store.js +1 -0
- package/models/theme-variables.d.ts +4 -189
- package/models/theme-variables.js +2 -191
- package/models/toolbar-store.d.ts +9 -0
- package/models/toolbar-store.js +1 -0
- package/models/ui-store.d.ts +6 -0
- package/models/ui-store.js +1 -0
- package/models/user.d.ts +3 -4
- package/models/vue-component.d.ts +4 -2
- package/package-lock.json +5553 -0
- package/package.json +37 -26
- package/remote-api/api.d.ts +288 -669
- package/remote-api/api.js +199 -485
- package/remote-api/base.js +1 -1
- package/remote-api/common.d.ts +1 -1
- package/sync/__tests__/memory-state.spec.d.ts +1 -0
- package/sync/__tests__/memory-state.spec.js +49 -0
- package/sync/__tests__/plan.spec.d.ts +1 -0
- package/sync/__tests__/plan.spec.js +116 -0
- package/sync/create-sync-plan.d.ts +2 -0
- package/sync/create-sync-plan.js +13 -0
- package/sync/fetch.d.ts +8 -0
- package/sync/fetch.js +32 -0
- package/sync/index.d.ts +10 -0
- package/sync/index.js +9 -0
- package/sync/memory-state.d.ts +2 -0
- package/sync/memory-state.js +22 -0
- package/sync/operations/conflict.d.ts +10 -0
- package/sync/operations/conflict.js +56 -0
- package/sync/operations/delete-local.d.ts +2 -0
- package/sync/operations/delete-local.js +17 -0
- package/sync/operations/delete-remote.d.ts +2 -0
- package/sync/operations/delete-remote.js +26 -0
- package/sync/operations/download.d.ts +2 -0
- package/sync/operations/download.js +20 -0
- package/sync/operations/index.d.ts +5 -0
- package/sync/operations/index.js +5 -0
- package/sync/operations/synced-file.d.ts +14 -0
- package/sync/operations/synced-file.js +5 -0
- package/sync/operations/upload.d.ts +2 -0
- package/sync/operations/upload.js +30 -0
- package/sync/plan.d.ts +9 -0
- package/sync/plan.js +57 -0
- package/sync/recovery.d.ts +2 -0
- package/sync/recovery.js +6 -0
- package/sync/scan.d.ts +4 -0
- package/sync/scan.js +40 -0
- package/sync/types.d.ts +74 -0
- package/sync/types.js +7 -0
- package/sync/utils/__tests__/oldest-synced-at.spec.d.ts +1 -0
- package/sync/utils/__tests__/oldest-synced-at.spec.js +38 -0
- package/sync/utils/oldest-synced-at.d.ts +2 -0
- package/sync/utils/oldest-synced-at.js +9 -0
- package/types/index.d.ts +0 -0
- package/types/index.js +0 -0
- package/utils/__tests__/find-files-diff.spec.d.ts +1 -0
- package/{tools → utils}/__tests__/find-files-diff.spec.js +3 -3
- package/utils/__tests__/find-note-files-diff.spec.d.ts +1 -0
- package/{tools → utils}/__tests__/find-note-files-diff.spec.js +5 -5
- package/utils/__tests__/get-file-name.spec.d.ts +1 -0
- package/utils/__tests__/get-string-path.spec.d.ts +1 -0
- package/utils/__tests__/is-gpg-encrypted.spec.d.ts +1 -0
- package/utils/__tests__/is-org-file.spec.d.ts +1 -0
- package/utils/__tests__/join.spec.d.ts +1 -0
- package/utils/__tests__/join.spec.js +32 -0
- package/utils/__tests__/nullable-guards.spec.d.ts +1 -0
- package/utils/__tests__/nullable-guards.spec.js +44 -0
- package/utils/__tests__/parent-folder.spec.d.ts +1 -0
- package/utils/__tests__/read-org-files-recursively.spec.d.ts +1 -0
- package/utils/__tests__/split-path.spec.d.ts +1 -0
- package/utils/__tests__/to-absolute-path.spec.d.ts +1 -0
- package/utils/__tests__/to-absolute-path.spec.js +26 -0
- package/utils/__tests__/to-error.spec.d.ts +1 -0
- package/utils/__tests__/to-error.spec.js +112 -0
- package/utils/__tests__/with-root.spec.d.ts +1 -0
- package/utils/__tests__/with-root.spec.js +20 -0
- package/{tools → utils}/find-notes-files-diff.js +6 -3
- package/{tools → utils}/index.d.ts +4 -1
- package/{tools → utils}/index.js +4 -1
- package/utils/join-path.d.ts +1 -0
- package/utils/join-path.js +13 -0
- package/utils/nullable-guards.d.ts +2 -0
- package/utils/nullable-guards.js +6 -0
- package/utils/to-absolute-path.d.ts +2 -0
- package/utils/to-absolute-path.js +2 -0
- package/utils/to-error.d.ts +6 -0
- package/utils/to-error.js +33 -0
- package/utils/toml.d.ts +3 -0
- package/utils/toml.js +31 -0
- package/utils/with-root.d.ts +1 -0
- package/utils/with-root.js +6 -0
- package/websocket/client.d.ts +24 -0
- package/websocket/client.js +83 -0
- package/models/file-tree.d.ts +0 -12
- package/tools/__tests__/join.spec.js +0 -24
- package/tools/join-path.d.ts +0 -1
- package/tools/join-path.js +0 -7
- package/tools/mock-server.d.ts +0 -1
- package/tools/mock-server.js +0 -12
- /package/models/{file-cache.js → buffer-store.js} +0 -0
- /package/models/{file-tree.js → buffer.js} +0 -0
- /package/{tools/__tests__/find-files-diff.spec.d.ts → models/colors.js} +0 -0
- /package/{tools/__tests__/find-note-files-diff.spec.d.ts → models/commands-group-store.js} +0 -0
- /package/{tools/__tests__/get-file-name.spec.d.ts → models/commands-store.js} +0 -0
- /package/{tools/__tests__/get-string-path.spec.d.ts → models/completion-store.js} +0 -0
- /package/{tools/__tests__/is-gpg-encrypted.spec.d.ts → models/config-store.js} +0 -0
- /package/{tools/__tests__/is-org-file.spec.d.ts → models/confirmation-modal.js} +0 -0
- /package/{tools/__tests__/join.spec.d.ts → models/context-menu-store.js} +0 -0
- /package/{tools/__tests__/parent-folder.spec.d.ts → models/cron-store.js} +0 -0
- /package/{tools/__tests__/read-org-files-recursively.spec.d.ts → models/cron-task.js} +0 -0
- /package/{tools/__tests__/split-path.spec.d.ts → models/css-utils.js} +0 -0
- /package/{tools → utils}/__tests__/get-file-name.spec.js +0 -0
- /package/{tools → utils}/__tests__/get-string-path.spec.js +0 -0
- /package/{tools → utils}/__tests__/is-gpg-encrypted.spec.js +0 -0
- /package/{tools → utils}/__tests__/is-org-file.spec.js +0 -0
- /package/{tools → utils}/__tests__/parent-folder.spec.js +0 -0
- /package/{tools → utils}/__tests__/read-org-files-recursively.spec.js +0 -0
- /package/{tools → utils}/__tests__/split-path.spec.js +0 -0
- /package/{tools → utils}/find-notes-files-diff.d.ts +0 -0
- /package/{tools → utils}/get-file-name.d.ts +0 -0
- /package/{tools → utils}/get-file-name.js +0 -0
- /package/{tools → utils}/get-parent-dir.d.ts +0 -0
- /package/{tools → utils}/get-parent-dir.js +0 -0
- /package/{tools → utils}/get-string-path.d.ts +0 -0
- /package/{tools → utils}/get-string-path.js +0 -0
- /package/{tools → utils}/is-gpg-encrypted.d.ts +0 -0
- /package/{tools → utils}/is-gpg-encrypted.js +0 -0
- /package/{tools → utils}/is-org-file.d.ts +0 -0
- /package/{tools → utils}/is-org-file.js +0 -0
- /package/{tools → utils}/split-path.d.ts +0 -0
- /package/{tools → utils}/split-path.js +0 -0
package/remote-api/base.js
CHANGED
package/remote-api/common.d.ts
CHANGED
|
@@ -62,4 +62,4 @@ export declare const toPathString: (url: URL) => string;
|
|
|
62
62
|
*
|
|
63
63
|
* @export
|
|
64
64
|
*/
|
|
65
|
-
export declare const createRequestFunction: (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) => <T = unknown, R = AxiosResponse<T
|
|
65
|
+
export declare const createRequestFunction: (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) => <T = unknown, R = AxiosResponse<T>>(axios?: AxiosInstance, basePath?: string) => Promise<R>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import { createMemorySyncState } from "../memory-state.js";
|
|
3
|
+
test('initial state empty', async () => {
|
|
4
|
+
const state = createMemorySyncState();
|
|
5
|
+
const data = await state.get();
|
|
6
|
+
expect(data.files).toEqual({});
|
|
7
|
+
});
|
|
8
|
+
test('initial with values', async () => {
|
|
9
|
+
const state = createMemorySyncState({
|
|
10
|
+
files: { 'a.org': { mtime: 1000, size: 100, status: 'synced' } },
|
|
11
|
+
});
|
|
12
|
+
const data = await state.get();
|
|
13
|
+
expect(data.files['a.org'].mtime).toBe(1000);
|
|
14
|
+
});
|
|
15
|
+
test('setFile and getFile', async () => {
|
|
16
|
+
const state = createMemorySyncState();
|
|
17
|
+
await state.setFile('b.org', { mtime: 2000, size: 200, status: 'dirty' });
|
|
18
|
+
const file = await state.getFile('b.org');
|
|
19
|
+
expect(file?.mtime).toBe(2000);
|
|
20
|
+
expect(file?.status).toBe('dirty');
|
|
21
|
+
});
|
|
22
|
+
test('getFile returns null for missing', async () => {
|
|
23
|
+
const state = createMemorySyncState();
|
|
24
|
+
const file = await state.getFile('missing.org');
|
|
25
|
+
expect(file).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
test('removeFile', async () => {
|
|
28
|
+
const state = createMemorySyncState();
|
|
29
|
+
await state.setFile('c.org', { mtime: 1000, size: 100, status: 'synced' });
|
|
30
|
+
await state.removeFile('c.org');
|
|
31
|
+
const file = await state.getFile('c.org');
|
|
32
|
+
expect(file).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
test('clear removes all', async () => {
|
|
35
|
+
const state = createMemorySyncState();
|
|
36
|
+
await state.setFile('d.org', { mtime: 1000, size: 100, status: 'synced' });
|
|
37
|
+
await state.setFile('e.org', { mtime: 2000, size: 200, status: 'synced' });
|
|
38
|
+
await state.clear();
|
|
39
|
+
const data = await state.get();
|
|
40
|
+
expect(data.files).toEqual({});
|
|
41
|
+
});
|
|
42
|
+
test('get returns copy', async () => {
|
|
43
|
+
const state = createMemorySyncState();
|
|
44
|
+
await state.setFile('f.org', { mtime: 1000, size: 100, status: 'synced' });
|
|
45
|
+
const data1 = await state.get();
|
|
46
|
+
data1.files['g.org'] = { mtime: 2000, size: 200, status: 'synced' };
|
|
47
|
+
const data2 = await state.get();
|
|
48
|
+
expect(data2.files['g.org']).toBeUndefined();
|
|
49
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import { createPlan } from "../plan.js";
|
|
3
|
+
const emptyState = { files: {} };
|
|
4
|
+
const serverTime = '2024-01-01T00:00:00Z';
|
|
5
|
+
test('new local file → upload', () => {
|
|
6
|
+
const localFiles = [{ path: 'a.org', mtime: 1000, size: 100 }];
|
|
7
|
+
const plan = createPlan({ localFiles, deletedLocally: [], remoteFiles: [], stateData: emptyState, serverTime });
|
|
8
|
+
expect(plan.toUpload).toHaveLength(1);
|
|
9
|
+
expect(plan.toUpload[0].path).toBe('a.org');
|
|
10
|
+
});
|
|
11
|
+
test('new remote file → download', () => {
|
|
12
|
+
const remoteFiles = [
|
|
13
|
+
{ path: 'b.org', version: 1, deleted: false, updatedAt: '' },
|
|
14
|
+
];
|
|
15
|
+
const plan = createPlan({ localFiles: [], deletedLocally: [], remoteFiles, stateData: emptyState, serverTime });
|
|
16
|
+
expect(plan.toDownload).toHaveLength(1);
|
|
17
|
+
expect(plan.toDownload[0].path).toBe('b.org');
|
|
18
|
+
});
|
|
19
|
+
test('unchanged file → skip', () => {
|
|
20
|
+
const localFiles = [{ path: 'c.org', mtime: 1000, size: 100 }];
|
|
21
|
+
const remoteFiles = [
|
|
22
|
+
{ path: 'c.org', version: 1, deleted: false, updatedAt: '' },
|
|
23
|
+
];
|
|
24
|
+
const stateData = {
|
|
25
|
+
files: { 'c.org': { mtime: 1000, size: 100, version: 1, status: 'synced' } },
|
|
26
|
+
};
|
|
27
|
+
const plan = createPlan({ localFiles, deletedLocally: [], remoteFiles, stateData, serverTime });
|
|
28
|
+
expect(plan.toUpload).toHaveLength(0);
|
|
29
|
+
expect(plan.toDownload).toHaveLength(0);
|
|
30
|
+
});
|
|
31
|
+
test('local changed → upload', () => {
|
|
32
|
+
const localFiles = [{ path: 'd.org', mtime: 2000, size: 100 }];
|
|
33
|
+
const remoteFiles = [
|
|
34
|
+
{ path: 'd.org', version: 1, deleted: false, updatedAt: '' },
|
|
35
|
+
];
|
|
36
|
+
const stateData = {
|
|
37
|
+
files: { 'd.org': { mtime: 1000, size: 100, version: 1, status: 'synced' } },
|
|
38
|
+
};
|
|
39
|
+
const plan = createPlan({ localFiles, deletedLocally: [], remoteFiles, stateData, serverTime });
|
|
40
|
+
expect(plan.toUpload).toHaveLength(1);
|
|
41
|
+
});
|
|
42
|
+
test('remote changed → download', () => {
|
|
43
|
+
const localFiles = [{ path: 'e.org', mtime: 1000, size: 100 }];
|
|
44
|
+
const remoteFiles = [
|
|
45
|
+
{ path: 'e.org', version: 2, deleted: false, updatedAt: '' },
|
|
46
|
+
];
|
|
47
|
+
const stateData = {
|
|
48
|
+
files: { 'e.org': { mtime: 1000, size: 100, version: 1, status: 'synced' } },
|
|
49
|
+
};
|
|
50
|
+
const plan = createPlan({ localFiles, deletedLocally: [], remoteFiles, stateData, serverTime });
|
|
51
|
+
expect(plan.toDownload).toHaveLength(1);
|
|
52
|
+
});
|
|
53
|
+
test('both changed → upload (conflict handled by server)', () => {
|
|
54
|
+
const localFiles = [{ path: 'f.org', mtime: 3000, size: 100 }];
|
|
55
|
+
const remoteFiles = [
|
|
56
|
+
{ path: 'f.org', version: 2, deleted: false, updatedAt: '1970-01-01T00:00:02.000Z' },
|
|
57
|
+
];
|
|
58
|
+
const stateData = {
|
|
59
|
+
files: { 'f.org': { mtime: 1000, size: 100, version: 1, status: 'synced' } },
|
|
60
|
+
};
|
|
61
|
+
const plan = createPlan({ localFiles, deletedLocally: [], remoteFiles, stateData, serverTime });
|
|
62
|
+
expect(plan.toUpload).toHaveLength(1);
|
|
63
|
+
expect(plan.toDownload).toHaveLength(0);
|
|
64
|
+
});
|
|
65
|
+
test('deleted locally → delete remote', () => {
|
|
66
|
+
const deletedLocally = ['g.org'];
|
|
67
|
+
const stateData = {
|
|
68
|
+
files: { 'g.org': { mtime: 1000, size: 100, version: 1, status: 'synced' } },
|
|
69
|
+
};
|
|
70
|
+
const plan = createPlan({ localFiles: [], deletedLocally, remoteFiles: [], stateData, serverTime });
|
|
71
|
+
expect(plan.toDeleteRemote).toContain('g.org');
|
|
72
|
+
});
|
|
73
|
+
test('deleted remotely → delete local', () => {
|
|
74
|
+
const localFiles = [{ path: 'h.org', mtime: 1000, size: 100 }];
|
|
75
|
+
const remoteFiles = [
|
|
76
|
+
{ path: 'h.org', version: 2, deleted: true, updatedAt: '' },
|
|
77
|
+
];
|
|
78
|
+
const stateData = {
|
|
79
|
+
files: { 'h.org': { mtime: 1000, size: 100, version: 1, status: 'synced' } },
|
|
80
|
+
};
|
|
81
|
+
const plan = createPlan({ localFiles, deletedLocally: [], remoteFiles, stateData, serverTime });
|
|
82
|
+
expect(plan.toDeleteLocal).toContain('h.org');
|
|
83
|
+
});
|
|
84
|
+
test('deleted locally but modified remotely → download', () => {
|
|
85
|
+
const deletedLocally = ['i.org'];
|
|
86
|
+
const remoteFiles = [
|
|
87
|
+
{ path: 'i.org', version: 2, deleted: false, updatedAt: '' },
|
|
88
|
+
];
|
|
89
|
+
const stateData = {
|
|
90
|
+
files: { 'i.org': { mtime: 1000, size: 100, version: 1, status: 'synced' } },
|
|
91
|
+
};
|
|
92
|
+
const plan = createPlan({ localFiles: [], deletedLocally, remoteFiles, stateData, serverTime });
|
|
93
|
+
expect(plan.toDownload).toHaveLength(1);
|
|
94
|
+
expect(plan.toDeleteRemote).toHaveLength(0);
|
|
95
|
+
});
|
|
96
|
+
test('deleted remotely but modified locally → upload (local changes win)', () => {
|
|
97
|
+
const localFiles = [{ path: 'j.org', mtime: 3000, size: 100 }];
|
|
98
|
+
const remoteFiles = [
|
|
99
|
+
{ path: 'j.org', version: 2, deleted: true, updatedAt: '1970-01-01T00:00:02.000Z' },
|
|
100
|
+
];
|
|
101
|
+
const stateData = {
|
|
102
|
+
files: { 'j.org': { mtime: 1000, size: 100, version: 1, status: 'synced' } },
|
|
103
|
+
};
|
|
104
|
+
const plan = createPlan({ localFiles, deletedLocally: [], remoteFiles, stateData, serverTime });
|
|
105
|
+
expect(plan.toUpload).toHaveLength(1);
|
|
106
|
+
expect(plan.toDeleteLocal).toHaveLength(0);
|
|
107
|
+
});
|
|
108
|
+
test('file with error status → retry upload', () => {
|
|
109
|
+
const localFiles = [{ path: 'k.org', mtime: 1000, size: 100 }];
|
|
110
|
+
const stateData = {
|
|
111
|
+
files: { 'k.org': { mtime: 1000, size: 100, version: 1, status: 'error', errorMessage: 'some error' } },
|
|
112
|
+
};
|
|
113
|
+
const plan = createPlan({ localFiles, deletedLocally: [], remoteFiles: [], stateData, serverTime });
|
|
114
|
+
expect(plan.toUpload).toHaveLength(1);
|
|
115
|
+
expect(plan.toUpload[0].path).toBe('k.org');
|
|
116
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { scanLocalFiles, findDeletedLocally } from "./scan.js";
|
|
2
|
+
import { fetchRemoteChanges } from "./fetch.js";
|
|
3
|
+
import { createPlan } from "./plan.js";
|
|
4
|
+
import { getOldestSyncedAt } from "./utils/oldest-synced-at.js";
|
|
5
|
+
export async function createSyncPlan(params) {
|
|
6
|
+
const { fs, api, state, rootPath, ignorePatterns } = params;
|
|
7
|
+
const stateData = await state.get();
|
|
8
|
+
const localFiles = await scanLocalFiles(fs, rootPath, ignorePatterns);
|
|
9
|
+
const deletedLocally = findDeletedLocally(localFiles, stateData);
|
|
10
|
+
const since = getOldestSyncedAt(stateData);
|
|
11
|
+
const { files: remoteFiles, serverTime } = await fetchRemoteChanges(api, since);
|
|
12
|
+
return createPlan({ localFiles, deletedLocally, remoteFiles, stateData, serverTime });
|
|
13
|
+
}
|
package/sync/fetch.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SyncChangesResponse } from '../remote-api/index.js';
|
|
2
|
+
import type { SyncApi, RemoteFile } from './types.js';
|
|
3
|
+
type ServerFields = Pick<SyncChangesResponse, 'serverTime' | 'cursor'>;
|
|
4
|
+
export interface FetchResult extends ServerFields {
|
|
5
|
+
files: RemoteFile[];
|
|
6
|
+
}
|
|
7
|
+
export declare const fetchRemoteChanges: (api: SyncApi, since?: string) => Promise<FetchResult>;
|
|
8
|
+
export {};
|
package/sync/fetch.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const DEFAULT_LIMIT = 100;
|
|
2
|
+
export const fetchRemoteChanges = async (api, since) => fetchAllPages(api, since);
|
|
3
|
+
const fetchAllPages = async (api, since, cursor, accumulated = []) => {
|
|
4
|
+
const page = await fetchPage(api, since, cursor);
|
|
5
|
+
const files = [...accumulated, ...page.files];
|
|
6
|
+
if (!page.cursor) {
|
|
7
|
+
return { files, serverTime: page.serverTime };
|
|
8
|
+
}
|
|
9
|
+
return fetchAllPages(api, since, page.cursor, files);
|
|
10
|
+
};
|
|
11
|
+
const toTimestamp = (isoString) => {
|
|
12
|
+
if (!isoString)
|
|
13
|
+
return undefined;
|
|
14
|
+
return new Date(isoString).getTime();
|
|
15
|
+
};
|
|
16
|
+
const fetchPage = async (api, since, cursor) => {
|
|
17
|
+
const sinceMs = toTimestamp(since);
|
|
18
|
+
const response = await api.syncChangesGet(sinceMs, DEFAULT_LIMIT, cursor);
|
|
19
|
+
const data = response.data.data;
|
|
20
|
+
return {
|
|
21
|
+
files: mapChangesToFiles(data.changes),
|
|
22
|
+
serverTime: data.serverTime,
|
|
23
|
+
cursor: data.hasMore ? data.cursor : undefined,
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
const mapChangesToFiles = (changes) => changes.map(toRemoteFile);
|
|
27
|
+
const toRemoteFile = (change) => ({
|
|
28
|
+
path: change.path,
|
|
29
|
+
version: change.version,
|
|
30
|
+
deleted: change.deleted,
|
|
31
|
+
updatedAt: change.updatedAt,
|
|
32
|
+
});
|
package/sync/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { createSyncPlan } from './create-sync-plan.js';
|
|
2
|
+
export { createPlan } from './plan.js';
|
|
3
|
+
export { createMemorySyncState } from './memory-state.js';
|
|
4
|
+
export { scanLocalFiles, findDeletedLocally } from './scan.js';
|
|
5
|
+
export { fetchRemoteChanges } from './fetch.js';
|
|
6
|
+
export { recoverState } from './recovery.js';
|
|
7
|
+
export { getOldestSyncedAt } from './utils/oldest-synced-at.js';
|
|
8
|
+
export { processUpload, processDownload, processDeleteLocal, processDeleteRemote, handleConflict, generateConflictPath, hasConflict, } from './operations/index.js';
|
|
9
|
+
export { SyncOperationType } from './types.js';
|
|
10
|
+
export type { SyncState, SyncStateData, SyncedFile, SyncStatus, LocalFile, RemoteFile, UploadResult, SyncPlan, SyncTask, SyncExecutor, SyncContext, CreateSyncPlanParams, } from './types.js';
|
package/sync/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { createSyncPlan } from "./create-sync-plan.js";
|
|
2
|
+
export { createPlan } from "./plan.js";
|
|
3
|
+
export { createMemorySyncState } from "./memory-state.js";
|
|
4
|
+
export { scanLocalFiles, findDeletedLocally } from "./scan.js";
|
|
5
|
+
export { fetchRemoteChanges } from "./fetch.js";
|
|
6
|
+
export { recoverState } from "./recovery.js";
|
|
7
|
+
export { getOldestSyncedAt } from "./utils/oldest-synced-at.js";
|
|
8
|
+
export { processUpload, processDownload, processDeleteLocal, processDeleteRemote, handleConflict, generateConflictPath, hasConflict, } from "./operations/index.js";
|
|
9
|
+
export { SyncOperationType } from "./types.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function createMemorySyncState(initial) {
|
|
2
|
+
const data = {
|
|
3
|
+
files: { ...initial?.files },
|
|
4
|
+
};
|
|
5
|
+
return {
|
|
6
|
+
async get() {
|
|
7
|
+
return { files: { ...data.files } };
|
|
8
|
+
},
|
|
9
|
+
async getFile(path) {
|
|
10
|
+
return data.files[path] ?? null;
|
|
11
|
+
},
|
|
12
|
+
async setFile(path, file) {
|
|
13
|
+
data.files[path] = { ...file };
|
|
14
|
+
},
|
|
15
|
+
async removeFile(path) {
|
|
16
|
+
delete data.files[path];
|
|
17
|
+
},
|
|
18
|
+
async clear() {
|
|
19
|
+
data.files = {};
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SyncContext, UploadResult } from '../types.js';
|
|
2
|
+
type ConflictUploadResult = Extract<UploadResult, {
|
|
3
|
+
status: 'conflict';
|
|
4
|
+
}>;
|
|
5
|
+
export declare const generateConflictPath: (path: string, deviceName?: string) => string;
|
|
6
|
+
export declare const handleConflict: (path: string, conflictResult: ConflictUploadResult, ctx: SyncContext) => Promise<void>;
|
|
7
|
+
export declare const hasConflict: (file: {
|
|
8
|
+
conflictPath?: string;
|
|
9
|
+
}) => boolean;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createSyncedFile } from "./synced-file.js";
|
|
2
|
+
export const generateConflictPath = (path, deviceName = 'device') => {
|
|
3
|
+
const lastDot = path.lastIndexOf('.');
|
|
4
|
+
const ext = lastDot >= 0 ? path.substring(lastDot) : '';
|
|
5
|
+
const base = lastDot >= 0 ? path.substring(0, lastDot) : path;
|
|
6
|
+
const timestamp = Date.now();
|
|
7
|
+
return `${base}.sync-conflict-${timestamp}-${deviceName}${ext}`;
|
|
8
|
+
};
|
|
9
|
+
const copyFile = async (fs, src, dest) => {
|
|
10
|
+
if (fs.copyFile) {
|
|
11
|
+
await fs.copyFile(src, dest);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const content = await fs.readFile(src, 'binary');
|
|
15
|
+
await fs.writeFile(dest, content);
|
|
16
|
+
};
|
|
17
|
+
const isNotFoundError = (error) => {
|
|
18
|
+
if (typeof error !== 'object' || error === null)
|
|
19
|
+
return false;
|
|
20
|
+
const axiosError = error;
|
|
21
|
+
return axiosError.response?.status === 404;
|
|
22
|
+
};
|
|
23
|
+
const tryDownloadServerVersion = async (path, serverVersion, ctx) => {
|
|
24
|
+
try {
|
|
25
|
+
await ctx.executor.download({
|
|
26
|
+
path,
|
|
27
|
+
version: serverVersion,
|
|
28
|
+
deleted: false,
|
|
29
|
+
updatedAt: new Date().toISOString(),
|
|
30
|
+
});
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (isNotFoundError(error))
|
|
35
|
+
return false;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
export const handleConflict = async (path, conflictResult, ctx) => {
|
|
40
|
+
const conflictPath = generateConflictPath(path, ctx.deviceName);
|
|
41
|
+
await copyFile(ctx.fs, path, conflictPath);
|
|
42
|
+
const downloaded = await tryDownloadServerVersion(path, conflictResult.serverVersion, ctx);
|
|
43
|
+
if (!downloaded) {
|
|
44
|
+
await ctx.fs.deleteFile(path);
|
|
45
|
+
await ctx.state.removeFile(path);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const fileInfo = await ctx.fs.fileInfo(path);
|
|
49
|
+
const meta = { mtime: fileInfo?.mtime ?? 0, size: fileInfo?.size ?? 0 };
|
|
50
|
+
await ctx.state.setFile(path, createSyncedFile(meta, {
|
|
51
|
+
version: conflictResult.serverVersion,
|
|
52
|
+
status: 'synced',
|
|
53
|
+
conflictPath,
|
|
54
|
+
}));
|
|
55
|
+
};
|
|
56
|
+
export const hasConflict = (file) => file.conflictPath !== undefined;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const processDeleteLocal = async (path, ctx) => {
|
|
2
|
+
try {
|
|
3
|
+
await ctx.fs.deleteFile(path);
|
|
4
|
+
await ctx.state.removeFile(path);
|
|
5
|
+
}
|
|
6
|
+
catch (error) {
|
|
7
|
+
const stored = await ctx.state.getFile(path);
|
|
8
|
+
if (stored) {
|
|
9
|
+
await ctx.state.setFile(path, {
|
|
10
|
+
...stored,
|
|
11
|
+
status: 'error',
|
|
12
|
+
errorMessage: String(error),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const isNotFoundError = (error) => {
|
|
2
|
+
if (!(error instanceof Error))
|
|
3
|
+
return false;
|
|
4
|
+
return error.message.includes('404') || error.message.includes('not found');
|
|
5
|
+
};
|
|
6
|
+
export const processDeleteRemote = async (path, ctx) => {
|
|
7
|
+
const stored = await ctx.state.getFile(path);
|
|
8
|
+
try {
|
|
9
|
+
await ctx.executor.deleteRemote(path, stored?.version ?? 0);
|
|
10
|
+
await ctx.state.removeFile(path);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (isNotFoundError(error)) {
|
|
14
|
+
await ctx.state.removeFile(path);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (stored) {
|
|
18
|
+
await ctx.state.setFile(path, {
|
|
19
|
+
...stored,
|
|
20
|
+
status: 'error',
|
|
21
|
+
errorMessage: String(error),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createSyncedFile } from "./synced-file.js";
|
|
2
|
+
const storedMeta = (stored) => ({
|
|
3
|
+
mtime: stored?.mtime ?? 0,
|
|
4
|
+
size: stored?.size ?? 0,
|
|
5
|
+
});
|
|
6
|
+
export const processDownload = async (file, ctx) => {
|
|
7
|
+
const stored = await ctx.state.getFile(file.path);
|
|
8
|
+
const meta = storedMeta(stored);
|
|
9
|
+
await ctx.state.setFile(file.path, createSyncedFile(meta, { version: stored?.version, status: 'downloading' }));
|
|
10
|
+
try {
|
|
11
|
+
await ctx.executor.download(file);
|
|
12
|
+
const fileInfo = await ctx.fs.fileInfo(file.path);
|
|
13
|
+
const downloadedMeta = { mtime: fileInfo?.mtime ?? 0, size: fileInfo?.size ?? 0 };
|
|
14
|
+
await ctx.state.setFile(file.path, createSyncedFile(downloadedMeta, { version: file.version, status: 'synced', syncedAt: ctx.serverTime }));
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
await ctx.state.setFile(file.path, createSyncedFile(meta, { version: stored?.version, status: 'error', errorMessage: String(error) }));
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { processUpload } from './upload.js';
|
|
2
|
+
export { processDownload } from './download.js';
|
|
3
|
+
export { processDeleteLocal } from './delete-local.js';
|
|
4
|
+
export { processDeleteRemote } from './delete-remote.js';
|
|
5
|
+
export { handleConflict, generateConflictPath, hasConflict } from './conflict.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { processUpload } from "./upload.js";
|
|
2
|
+
export { processDownload } from "./download.js";
|
|
3
|
+
export { processDeleteLocal } from "./delete-local.js";
|
|
4
|
+
export { processDeleteRemote } from "./delete-remote.js";
|
|
5
|
+
export { handleConflict, generateConflictPath, hasConflict } from "./conflict.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SyncedFile, SyncStatus } from '../types.js';
|
|
2
|
+
interface FileMeta {
|
|
3
|
+
mtime: number;
|
|
4
|
+
size: number;
|
|
5
|
+
}
|
|
6
|
+
export interface SyncedFileOptions {
|
|
7
|
+
version?: number;
|
|
8
|
+
status: SyncStatus;
|
|
9
|
+
syncedAt?: string;
|
|
10
|
+
errorMessage?: string;
|
|
11
|
+
conflictPath?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const createSyncedFile: (meta: FileMeta, options: SyncedFileOptions) => SyncedFile;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { handleConflict } from "./conflict.js";
|
|
2
|
+
import { createSyncedFile } from "./synced-file.js";
|
|
3
|
+
const executeUpload = async (file, expectedVersion, ctx) => {
|
|
4
|
+
const result = await ctx.executor.upload(file, expectedVersion);
|
|
5
|
+
if (result.status === 'ok') {
|
|
6
|
+
await ctx.state.setFile(file.path, createSyncedFile(file, {
|
|
7
|
+
version: result.version,
|
|
8
|
+
status: 'synced',
|
|
9
|
+
syncedAt: ctx.serverTime,
|
|
10
|
+
}));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
await handleConflict(file.path, result, ctx);
|
|
14
|
+
};
|
|
15
|
+
export const processUpload = async (file, ctx) => {
|
|
16
|
+
const stored = await ctx.state.getFile(file.path);
|
|
17
|
+
const expectedVersion = stored?.version;
|
|
18
|
+
await ctx.state.setFile(file.path, createSyncedFile(file, { version: expectedVersion, status: 'uploading' }));
|
|
19
|
+
try {
|
|
20
|
+
await executeUpload(file, expectedVersion, ctx);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
await ctx.state.setFile(file.path, createSyncedFile(file, {
|
|
24
|
+
version: expectedVersion,
|
|
25
|
+
status: 'error',
|
|
26
|
+
errorMessage: String(error),
|
|
27
|
+
}));
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
};
|
package/sync/plan.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LocalFile, RemoteFile, SyncPlan, SyncStateData } from './types.js';
|
|
2
|
+
export interface CreatePlanParams {
|
|
3
|
+
localFiles: LocalFile[];
|
|
4
|
+
deletedLocally: string[];
|
|
5
|
+
remoteFiles: RemoteFile[];
|
|
6
|
+
stateData: SyncStateData;
|
|
7
|
+
serverTime: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const createPlan: ({ localFiles, deletedLocally, remoteFiles, stateData, serverTime, }: CreatePlanParams) => SyncPlan;
|
package/sync/plan.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { SyncOperationType } from "./types.js";
|
|
2
|
+
export const createPlan = ({ localFiles, deletedLocally, remoteFiles, stateData, serverTime, }) => {
|
|
3
|
+
const index = buildFileIndex(localFiles, deletedLocally, remoteFiles);
|
|
4
|
+
const localActions = localFiles.map(local => resolveLocalFile(local, index.remoteByPath.get(local.path), stateData.files[local.path]));
|
|
5
|
+
const deletedActions = deletedLocally.map(path => resolveDeletedLocally(path, index.remoteByPath.get(path), stateData.files[path]));
|
|
6
|
+
const remoteActions = remoteFiles
|
|
7
|
+
.filter(remote => isNewRemote(remote, index))
|
|
8
|
+
.map(resolveNewRemote);
|
|
9
|
+
return buildPlanFromActions([...localActions, ...deletedActions, ...remoteActions], serverTime);
|
|
10
|
+
};
|
|
11
|
+
const buildFileIndex = (localFiles, deletedLocally, remoteFiles) => ({
|
|
12
|
+
remoteByPath: new Map(remoteFiles.map(f => [f.path, f])),
|
|
13
|
+
localByPath: new Map(localFiles.map(f => [f.path, f])),
|
|
14
|
+
deletedSet: new Set(deletedLocally),
|
|
15
|
+
});
|
|
16
|
+
const isNewRemote = (remote, index) => !index.localByPath.has(remote.path) && !index.deletedSet.has(remote.path);
|
|
17
|
+
const buildPlanFromActions = (actions, serverTime) => actions.reduce((plan, action) => applyAction(plan, action), { toUpload: [], toDownload: [], toDeleteLocal: [], toDeleteRemote: [], serverTime });
|
|
18
|
+
const actionHandlers = {
|
|
19
|
+
[SyncOperationType.Upload]: (plan, action) => ({ ...plan, toUpload: [...plan.toUpload, action.file] }),
|
|
20
|
+
[SyncOperationType.Download]: (plan, action) => ({ ...plan, toDownload: [...plan.toDownload, action.file] }),
|
|
21
|
+
[SyncOperationType.DeleteLocal]: (plan, action) => ({ ...plan, toDeleteLocal: [...plan.toDeleteLocal, action.path] }),
|
|
22
|
+
[SyncOperationType.DeleteRemote]: (plan, action) => ({ ...plan, toDeleteRemote: [...plan.toDeleteRemote, action.path] }),
|
|
23
|
+
none: (plan) => plan,
|
|
24
|
+
};
|
|
25
|
+
const applyAction = (plan, action) => actionHandlers[action.type](plan, action);
|
|
26
|
+
const resolveLocalFile = (local, remote, stored) => {
|
|
27
|
+
const localChanged = isLocalChanged(local, stored);
|
|
28
|
+
if (!remote) {
|
|
29
|
+
return localChanged ? upload(local) : none();
|
|
30
|
+
}
|
|
31
|
+
if (remote.deleted) {
|
|
32
|
+
return localChanged ? upload(local) : deleteLocal(local.path);
|
|
33
|
+
}
|
|
34
|
+
const remoteChanged = isRemoteChanged(remote, stored);
|
|
35
|
+
if (localChanged && remoteChanged) {
|
|
36
|
+
return upload(local);
|
|
37
|
+
}
|
|
38
|
+
if (localChanged)
|
|
39
|
+
return upload(local);
|
|
40
|
+
if (remoteChanged)
|
|
41
|
+
return download(remote);
|
|
42
|
+
return none();
|
|
43
|
+
};
|
|
44
|
+
const resolveDeletedLocally = (path, remote, stored) => {
|
|
45
|
+
if (!remote || remote.deleted) {
|
|
46
|
+
return deleteRemote(path);
|
|
47
|
+
}
|
|
48
|
+
return isRemoteChanged(remote, stored) ? download(remote) : deleteRemote(path);
|
|
49
|
+
};
|
|
50
|
+
const resolveNewRemote = (remote) => remote.deleted ? none() : download(remote);
|
|
51
|
+
const isLocalChanged = (local, stored) => !stored || local.mtime !== stored.mtime || stored.status === 'error';
|
|
52
|
+
const isRemoteChanged = (remote, stored) => !stored || remote.version > (stored.version ?? 0);
|
|
53
|
+
const upload = (file) => ({ type: SyncOperationType.Upload, file });
|
|
54
|
+
const download = (file) => ({ type: SyncOperationType.Download, file });
|
|
55
|
+
const deleteLocal = (path) => ({ type: SyncOperationType.DeleteLocal, path });
|
|
56
|
+
const deleteRemote = (path) => ({ type: SyncOperationType.DeleteRemote, path });
|
|
57
|
+
const none = () => ({ type: 'none' });
|
package/sync/recovery.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
const isInterruptedStatus = (status) => status === 'uploading' || status === 'downloading';
|
|
2
|
+
export const recoverState = async (state) => {
|
|
3
|
+
const data = await state.get();
|
|
4
|
+
const interruptedFiles = Object.entries(data.files).filter(([, file]) => isInterruptedStatus(file.status));
|
|
5
|
+
await Promise.all(interruptedFiles.map(([path, file]) => state.setFile(path, { ...file, status: 'dirty' })));
|
|
6
|
+
};
|
package/sync/scan.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FileSystem } from '../models/file-system.js';
|
|
2
|
+
import type { LocalFile, SyncStateData } from './types.js';
|
|
3
|
+
export declare function scanLocalFiles(fs: FileSystem, rootPath: string, ignorePatterns?: string[]): Promise<LocalFile[]>;
|
|
4
|
+
export declare const findDeletedLocally: (localFiles: LocalFile[], stateData: SyncStateData) => string[];
|
package/sync/scan.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { toAbsolutePath } from "../utils/to-absolute-path.js";
|
|
2
|
+
const DEFAULT_IGNORE = [
|
|
3
|
+
'.git',
|
|
4
|
+
'.DS_Store',
|
|
5
|
+
'node_modules',
|
|
6
|
+
'.sync-state',
|
|
7
|
+
'.Trash',
|
|
8
|
+
];
|
|
9
|
+
export async function scanLocalFiles(fs, rootPath, ignorePatterns = []) {
|
|
10
|
+
const ignore = [...DEFAULT_IGNORE, ...ignorePatterns];
|
|
11
|
+
return scanDir(fs, rootPath, ignore);
|
|
12
|
+
}
|
|
13
|
+
async function scanDir(fs, path, ignore) {
|
|
14
|
+
const entries = await fs.readDir(path);
|
|
15
|
+
const filteredEntries = entries.filter((e) => !shouldIgnore(e.name, ignore));
|
|
16
|
+
const nestedResults = await Promise.all(filteredEntries.map((entry) => processEntry(fs, entry, ignore)));
|
|
17
|
+
return nestedResults.flat();
|
|
18
|
+
}
|
|
19
|
+
async function processEntry(fs, entry, ignore) {
|
|
20
|
+
if (entry.type === 'directory') {
|
|
21
|
+
return scanDir(fs, entry.path, ignore);
|
|
22
|
+
}
|
|
23
|
+
return [toLocalFile(entry)];
|
|
24
|
+
}
|
|
25
|
+
const toLocalFile = (entry) => ({
|
|
26
|
+
path: toAbsolutePath(entry.path),
|
|
27
|
+
mtime: entry.mtime,
|
|
28
|
+
size: entry.size,
|
|
29
|
+
});
|
|
30
|
+
const shouldIgnore = (name, patterns) => patterns.some((pattern) => matchPattern(pattern, name));
|
|
31
|
+
const matchPattern = (pattern, name) => {
|
|
32
|
+
if (!pattern.includes('*'))
|
|
33
|
+
return name === pattern;
|
|
34
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
35
|
+
return regex.test(name);
|
|
36
|
+
};
|
|
37
|
+
export const findDeletedLocally = (localFiles, stateData) => {
|
|
38
|
+
const localPaths = new Set(localFiles.map((f) => f.path));
|
|
39
|
+
return Object.keys(stateData.files).filter((path) => !localPaths.has(path));
|
|
40
|
+
};
|