orgnote-api 0.16.0 → 0.17.1

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.
@@ -46,22 +46,6 @@ exports[`Should decrypt note via provided keys 1`] = `
46
46
  ]
47
47
  `;
48
48
 
49
- exports[`Should encrypt note and decrypt it into binary format 1`] = `
50
- {
51
- "author": {
52
- "email": "test@mail.com",
53
- "id": "1",
54
- "name": "John Doe",
55
- },
56
- "encrypted": true,
57
- "id": "id",
58
- "meta": {
59
- "id": undefined,
60
- "published": false,
61
- },
62
- }
63
- `;
64
-
65
49
  exports[`Should encrypt note content via password 1`] = `
66
50
  {
67
51
  "author": {
@@ -19,10 +19,11 @@ import {
19
19
  armoredPrivateKey,
20
20
  privateKeyPassphrase,
21
21
  } from './encryption-keys';
22
- // import { armor } from 'openpgp';
22
+ import { ModelsPublicNoteEncryptionTypeEnum } from 'src/remote-api';
23
23
 
24
24
  test('Should encrypt text as armored message via keys', async () => {
25
25
  const res = await encryptViaKeys({
26
+ type: ModelsPublicNoteEncryptionTypeEnum.GpgKeys,
26
27
  content: 'Hello world',
27
28
  publicKey: armoredPublicKey,
28
29
  privateKey: armoredPrivateKey,
@@ -35,6 +36,7 @@ test('Should encrypt text as armored message via keys', async () => {
35
36
 
36
37
  test('Should encrypt text via keys', async () => {
37
38
  const res = await encryptViaKeys({
39
+ type: ModelsPublicNoteEncryptionTypeEnum.GpgKeys,
38
40
  content: 'Hello world',
39
41
  publicKey: armoredPublicKey,
40
42
  privateKey: armoredPrivateKey,
@@ -88,6 +90,7 @@ YQ==
88
90
  test('Should encrypt via password', async () => {
89
91
  const password = 'test';
90
92
  const res = await encryptViaPassword({
93
+ type: ModelsPublicNoteEncryptionTypeEnum.GpgPassword,
91
94
  content: 'Hello world',
92
95
  password,
93
96
  format: 'armored',
@@ -325,7 +328,28 @@ test('Should armor and unarmor encrypted file', async () => {
325
328
  "
326
329
  `);
327
330
 
328
- const { text, data } = await unarmor(armored);
331
+ const { data } = await unarmor(armored);
329
332
 
330
333
  expect(data).toEqual(content);
331
334
  });
335
+
336
+ test('Should decrypt value from provided real world data and passwords', async () => {
337
+ const text = `Hello world!`;
338
+ const password = 'qweqwebebe1';
339
+
340
+ const encryptedContent = await encrypt({
341
+ content: text,
342
+ type: 'gpgPassword',
343
+ password,
344
+ });
345
+
346
+ const armoredContent = armor(encryptedContent as unknown as Uint8Array);
347
+
348
+ const decryptedMessage = await decrypt({
349
+ content: armoredContent,
350
+ type: 'gpgPassword',
351
+ password,
352
+ });
353
+
354
+ expect(decryptedMessage).toEqual('Hello world!');
355
+ });
@@ -289,7 +289,6 @@ test('Should not decrypt note without provided encrypted type', async () => {
289
289
  content: noteText,
290
290
  type: 'gpgPassword',
291
291
  password: '123',
292
- format: 'armored',
293
292
  });
294
293
 
295
294
  expect(decryptedInfo).toMatchSnapshot();
@@ -177,6 +177,7 @@ async function _decryptViaPassword<
177
177
  >;
178
178
  }
179
179
 
180
+ // TODO: master OrgNoteGpgDecryption type
180
181
  async function _decryptViaKeys<
181
182
  T extends Omit<WithDecryptionContent<OrgNoteGpgEncryption>, 'type'>,
182
183
  >({
@@ -19,6 +19,7 @@ export interface AbstractEncryptedNote {
19
19
  }
20
20
 
21
21
  export type EncryptionResult<T> = Promise<[T, string]>;
22
+ export type DecryptionResult<T> = Promise<[T, string | Uint8Array]>;
22
23
 
23
24
  // TODO: master change signature for encrypt notes without content
24
25
  export async function encryptNote<T extends AbstractEncryptedNote>(
@@ -46,7 +47,7 @@ export async function encryptNote<T extends AbstractEncryptedNote>(
46
47
  export async function decryptNote<T extends AbstractEncryptedNote>(
47
48
  note: T,
48
49
  encryptionParams: WithNoteDecryptionContent<OrgNoteEncryption>
49
- ): EncryptionResult<T> {
50
+ ): DecryptionResult<T> {
50
51
  const isContentEncrypted = isGpgEncrypted(encryptionParams.content);
51
52
  if (
52
53
  note.meta.published ||
@@ -15,7 +15,7 @@ export interface OrgNoteGpgEncryption {
15
15
  /* Armored private key */
16
16
  privateKey: string;
17
17
  /* Armored public key */
18
- publicKey: string;
18
+ publicKey?: string;
19
19
  privateKeyPassphrase?: string;
20
20
  }
21
21
 
@@ -41,10 +41,14 @@ export type WithEncryptionContent<
41
41
  T extends OrgNoteEncryption = OrgNoteEncryption,
42
42
  > = T & EncryptionData & BaseOrgNoteEncryption;
43
43
 
44
+ export type DecriptionData = {
45
+ content: string | Uint8Array;
46
+ };
47
+
44
48
  export type WithDecryptionContent<
45
49
  T extends OrgNoteEncryption = OrgNoteEncryption,
46
- > = T & EncryptionData & BaseOrgNoteDecryption;
50
+ > = T & DecriptionData & BaseOrgNoteDecryption;
47
51
 
48
52
  export type WithNoteDecryptionContent<
49
53
  T extends OrgNoteEncryption = OrgNoteEncryption,
50
- > = T & EncryptionData;
54
+ > = T & DecriptionData;
@@ -0,0 +1,30 @@
1
+ export interface FileInfo {
2
+ name: string;
3
+ path: string;
4
+ type: 'directory' | 'file';
5
+ size: number;
6
+ atime?: number;
7
+ ctime?: number;
8
+ mtime: number;
9
+ uri?: string;
10
+ }
11
+
12
+ export interface FileSystem {
13
+ readFile: <T extends 'utf8'>(
14
+ path: string,
15
+ encoding?: T
16
+ ) => Promise<T extends 'utf8' ? string : Uint8Array>;
17
+ writeFile: (
18
+ path: string,
19
+ content: string | Uint8Array,
20
+ encoding?: BufferEncoding
21
+ ) => Promise<void>;
22
+ readDir: (path: string) => Promise<FileInfo[]>;
23
+ fileInfo: (path: string) => Promise<FileInfo>;
24
+ rename: (path: string, newPath: string) => Promise<void>;
25
+ deleteFile: (path: string) => Promise<void>;
26
+ rmdir: (path: string) => Promise<void>;
27
+ mkdir: (path: string) => Promise<void>;
28
+ isDirExist: (path: string) => Promise<boolean>;
29
+ isFileExist: (path: string) => Promise<boolean>;
30
+ }
package/models/index.ts CHANGED
@@ -9,3 +9,5 @@ export * from './widget-type';
9
9
  export * from './editor';
10
10
  export * from './default-commands';
11
11
  export * from './encryption';
12
+ export * from './file-system';
13
+ export * from './sync';
package/models/sync.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { HandlersCreatingNote } from 'src/remote-api';
2
+ import { FileSystem } from './file-system';
3
+
4
+ export interface Changes {
5
+ deleted: string[];
6
+ created: string[];
7
+ updated: string[];
8
+ }
9
+
10
+ export interface NoteChange {
11
+ filePath: string;
12
+ id?: string;
13
+ }
14
+
15
+ export interface NoteChanges {
16
+ deleted: NoteChange[];
17
+ created: NoteChange[];
18
+ updated: NoteChange[];
19
+ }
20
+
21
+ export interface FullContextNoteChanges
22
+ extends Omit<NoteChanges, 'created' | 'updated'> {
23
+ created: HandlersCreatingNote[];
24
+ updated: HandlersCreatingNote[];
25
+ }
26
+
27
+ export interface StoredNoteInfo {
28
+ filePath: string[];
29
+ id: string;
30
+ updatedAt: string | Date;
31
+ }
32
+
33
+ export interface FileScanParams {
34
+ readDir: FileSystem['readDir'];
35
+ fileInfo: FileSystem['fileInfo'];
36
+ dirPath: string;
37
+ }
38
+
39
+ export interface SyncParams extends FileScanParams {
40
+ storedNotesInfo: StoredNoteInfo[];
41
+ /* Will be used instead of updated time from the storedNoteInfo property */
42
+ lastSync?: Date;
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orgnote-api",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
4
4
  "description": "Official API for creating extensions for OrgNote app",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -45,6 +45,7 @@
45
45
  "axios": "1.7.3",
46
46
  "openpgp": "5.11.1",
47
47
  "org-mode-ast": "0.11.7",
48
+ "orgnote-api": "0.16.0",
48
49
  "vue": "3.4.15",
49
50
  "vue-router": "4.2.5"
50
51
  },
@@ -0,0 +1,9 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`Should join file paths 1`] = `"dir1/subdir2/file"`;
4
+
5
+ exports[`Should join file paths 2`] = `"dir2/file"`;
6
+
7
+ exports[`Should join file paths 3`] = `"/dri3/subdir2/file"`;
8
+
9
+ exports[`Should join file paths 4`] = `"file"`;
@@ -0,0 +1,177 @@
1
+ import { afterEach, beforeEach, expect, test } from 'vitest';
2
+ import { mkdirSync, rmdirSync, statSync, utimesSync, writeFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { findFilesDiff } from '../find-notes-files-diff';
5
+ import { StoredNoteInfo } from '../../models';
6
+
7
+ const testFilesFolder = 'src/tools/__tests__/miscellaneous2/';
8
+
9
+ function initFiles(): void {
10
+ mkdirSync(testFilesFolder);
11
+ mkdirSync(testFilesFolder + '/nested-folder');
12
+ }
13
+
14
+ function cleanFiles(): void {
15
+ try {
16
+ rmdirSync(testFilesFolder, { recursive: true });
17
+ } catch (e) {}
18
+ }
19
+
20
+ function createTestFile(
21
+ content: string,
22
+ fileName: string,
23
+ updated: Date
24
+ ): void {
25
+ const filePath = join(testFilesFolder, fileName);
26
+
27
+ writeFileSync(filePath, content);
28
+
29
+ const time = updated.getTime() / 1000; // Convert to seconds
30
+ utimesSync(filePath, time, time);
31
+ }
32
+
33
+ beforeEach(() => initFiles());
34
+ afterEach(() => cleanFiles());
35
+
36
+ test('Should find files diff', () => {
37
+ createTestFile(`unchanged file!`, 'org-file.org', new Date('2021-01-01'));
38
+ createTestFile(
39
+ `changed org file 2!`,
40
+ 'org-file2.org',
41
+ new Date('2022-01-01')
42
+ );
43
+ createTestFile(
44
+ `deleted org in the nested folder file!`,
45
+ 'nested-folder/org-file3.org',
46
+ new Date('2021-01-01')
47
+ );
48
+ createTestFile(
49
+ `created file in the nested folder!`,
50
+ 'nested-folder/org-file4.org',
51
+ new Date('2021-02-01')
52
+ );
53
+
54
+ const storedNoteInfos: StoredNoteInfo[] = [
55
+ {
56
+ filePath: [testFilesFolder + 'deleted-org-file.org'],
57
+ id: 'deleted-org-file.org',
58
+ updatedAt: new Date('2021-01-01'),
59
+ },
60
+ {
61
+ filePath: [testFilesFolder + 'org-file.org'],
62
+ id: 'org-file.org',
63
+ updatedAt: new Date('2021-01-01'),
64
+ },
65
+ {
66
+ filePath: [testFilesFolder + 'org-file2.org'],
67
+ id: 'org-file2.org',
68
+ updatedAt: new Date('2021-01-01'),
69
+ },
70
+ {
71
+ filePath: [testFilesFolder + 'nested-folder/org-file3.org'],
72
+ id: 'nested-folder/org-file3.org',
73
+ updatedAt: new Date('2021-01-01'),
74
+ },
75
+ ];
76
+
77
+ const changedFiles = findFilesDiff(
78
+ [
79
+ testFilesFolder + 'org-file.org',
80
+ testFilesFolder + 'org-file2.org',
81
+ testFilesFolder + 'nested-folder/org-file3.org',
82
+ testFilesFolder + 'nested-folder/org-file4.org',
83
+ ],
84
+ storedNoteInfos,
85
+ (filePath: string) => new Date(statSync(filePath).mtime)
86
+ );
87
+
88
+ expect(changedFiles).toEqual({
89
+ created: [testFilesFolder + 'nested-folder/org-file4.org'],
90
+ updated: [testFilesFolder + 'org-file2.org'],
91
+ deleted: [testFilesFolder + 'deleted-org-file.org'],
92
+ });
93
+ });
94
+
95
+ test('Should find files diff when folder was renamed', () => {
96
+ createTestFile(
97
+ `nested file!`,
98
+ 'nested-folder/org-file.org',
99
+ new Date('2021-01-01')
100
+ );
101
+ createTestFile(
102
+ `nested file2!`,
103
+ 'nested-folder/org-file-2.org',
104
+ new Date('2021-01-01')
105
+ );
106
+
107
+ const storedNoteInfos: StoredNoteInfo[] = [
108
+ {
109
+ filePath: [testFilesFolder + 'nested-folder/org-file.org'],
110
+ id: 'nested-folder/org-file.org',
111
+ updatedAt: new Date('2021-01-01'),
112
+ },
113
+ {
114
+ filePath: [testFilesFolder + 'nested-folder/org-file2.org'],
115
+ id: 'nested-folder/org-file2.org',
116
+ updatedAt: new Date('2021-01-01'),
117
+ },
118
+ ];
119
+
120
+ rmdirSync(testFilesFolder + 'nested-folder', { recursive: true });
121
+
122
+ const changedFiles = findFilesDiff(
123
+ [],
124
+ storedNoteInfos,
125
+ (filePath: string) => new Date(statSync(filePath).mtime)
126
+ );
127
+
128
+ expect(changedFiles).toMatchInlineSnapshot(`
129
+ {
130
+ "created": [],
131
+ "deleted": [
132
+ "src/tools/__tests__/miscellaneous2/nested-folder/org-file.org",
133
+ "src/tools/__tests__/miscellaneous2/nested-folder/org-file2.org",
134
+ ],
135
+ "updated": [],
136
+ }
137
+ `);
138
+ });
139
+
140
+ test('Should find created note when nested folder created', () => {
141
+ mkdirSync(testFilesFolder + 'new-nested-folder');
142
+ createTestFile(
143
+ `new nested file!`,
144
+ 'new-nested-folder/org-file.org',
145
+ new Date('2023-01-01')
146
+ );
147
+
148
+ const storedNoteInfos: StoredNoteInfo[] = [];
149
+
150
+ const changedFiles = findFilesDiff(
151
+ [testFilesFolder + 'new-nested-folder/org-file.org'],
152
+ storedNoteInfos,
153
+ (filePath: string) => new Date(statSync(filePath).mtime)
154
+ );
155
+
156
+ expect(changedFiles).toEqual({
157
+ created: [testFilesFolder + 'new-nested-folder/org-file.org'],
158
+ updated: [],
159
+ deleted: [],
160
+ });
161
+ });
162
+
163
+ test('Should find nothing when no files provided', () => {
164
+ const storedNoteInfos: StoredNoteInfo[] = [];
165
+
166
+ const changedFiles = findFilesDiff(
167
+ [],
168
+ storedNoteInfos,
169
+ (filePath: string) => new Date(statSync(filePath).mtime)
170
+ );
171
+
172
+ expect(changedFiles).toEqual({
173
+ created: [],
174
+ updated: [],
175
+ deleted: [],
176
+ });
177
+ });
@@ -0,0 +1,207 @@
1
+ import { mkdirSync, writeFileSync, utimesSync, readdirSync } from 'fs';
2
+ import { afterEach, beforeEach, test, expect } from 'vitest';
3
+ import { FileInfo, StoredNoteInfo } from '../../models';
4
+ import { findNoteFilesDiff } from '../find-notes-files-diff';
5
+ import { statSync } from 'fs';
6
+ import { Stats } from 'fs';
7
+ import { rmSync } from 'fs';
8
+ import { getFileName } from '../get-file-name';
9
+ import { join } from '../join';
10
+
11
+ const testFilesFolder = 'src/tools/__tests__/miscellaneous/';
12
+ const nestedFolder = 'nested-folder/';
13
+ const fn = (fileName: string) => `${testFilesFolder}${fileName}`;
14
+ const fns = (fileName: string) =>
15
+ `${testFilesFolder}${nestedFolder}${fileName}`;
16
+
17
+ function initFiles(): void {
18
+ mkdirSync(testFilesFolder.slice(0, -1));
19
+ mkdirSync(fn(nestedFolder));
20
+ }
21
+
22
+ function cleanFiles(): void {
23
+ try {
24
+ rmSync(testFilesFolder, { recursive: true });
25
+ } catch (e) {}
26
+ }
27
+
28
+ function createTestFile(
29
+ filePath: string,
30
+ updated: Date,
31
+ content: string
32
+ ): void {
33
+ writeFileSync(filePath, content);
34
+
35
+ const time = updated.getTime() / 1000; // Convert to seconds
36
+ utimesSync(filePath, time, time);
37
+ }
38
+
39
+ beforeEach(() => initFiles());
40
+ afterEach(() => cleanFiles());
41
+
42
+ const fileStatToFileInfo = (path: string, stat: Stats): FileInfo => ({
43
+ path,
44
+ name: getFileName(path),
45
+ type: stat.isFile() ? 'file' : 'directory',
46
+ size: stat.size,
47
+ atime: stat.atime.getTime(),
48
+ mtime: stat.mtime.getTime(),
49
+ ctime: stat.ctime.getTime(),
50
+ });
51
+
52
+ test('Should sync notes files', async () => {
53
+ const files = [
54
+ [fn('file-1.org')],
55
+ [fn('file-2.org')],
56
+ [fn('file-9.org'), new Date('2024-10-10')],
57
+ [fns('file-3.org')],
58
+ [fns('file-4.org'), new Date('2024-08-09')],
59
+ ] as const;
60
+
61
+ files.forEach(([f, d], i) =>
62
+ createTestFile(
63
+ f,
64
+ d ?? new Date('2024-01-01'),
65
+ `:PROPERTIES:
66
+ :ID: ${i + 1}
67
+ :PROPERTIES:
68
+
69
+ * Hello world from note:
70
+ - ${f}`
71
+ )
72
+ );
73
+
74
+ const storedNotesInfo: StoredNoteInfo[] = [
75
+ {
76
+ id: '2',
77
+ filePath: fn('file-1.org').split('/'),
78
+ updatedAt: new Date('2024-01-01'),
79
+ },
80
+ {
81
+ id: '3',
82
+ filePath: fns('file-3.org').split('/'),
83
+ updatedAt: new Date('2024-01-01'),
84
+ },
85
+ {
86
+ id: '9',
87
+ filePath: fn('file-9.org').split('/'),
88
+ updatedAt: new Date('2020-01-01'),
89
+ },
90
+ {
91
+ id: '10',
92
+ filePath: fns('file-10.org').split('/'),
93
+ updatedAt: new Date('2020-01-01'),
94
+ },
95
+ ];
96
+
97
+ const notesFilesDiff = await findNoteFilesDiff({
98
+ fileInfo: async (path: string) => {
99
+ const stats = statSync(path);
100
+
101
+ const fileInfo: FileInfo = fileStatToFileInfo(path, stats);
102
+
103
+ return Promise.resolve(fileInfo);
104
+ },
105
+ readDir: async (path: string) => {
106
+ const dirents = readdirSync(path, { withFileTypes: true });
107
+
108
+ const fileInfos: FileInfo[] = dirents.map((dirent) => {
109
+ const stats = statSync(join(path, dirent.name));
110
+ const fileInfo = fileStatToFileInfo(join(path, dirent.name), stats);
111
+ return fileInfo;
112
+ });
113
+
114
+ return Promise.resolve(fileInfos);
115
+ },
116
+ storedNotesInfo,
117
+ dirPath: testFilesFolder,
118
+ });
119
+
120
+ expect(notesFilesDiff).toMatchInlineSnapshot(`
121
+ {
122
+ "created": [
123
+ {
124
+ "filePath": "src/tools/__tests__/miscellaneous/file-2.org",
125
+ },
126
+ {
127
+ "filePath": "src/tools/__tests__/miscellaneous/nested-folder/file-4.org",
128
+ },
129
+ ],
130
+ "deleted": [
131
+ {
132
+ "filePath": "src/tools/__tests__/miscellaneous/nested-folder/file-10.org",
133
+ "id": "10",
134
+ },
135
+ ],
136
+ "updated": [
137
+ {
138
+ "filePath": "src/tools/__tests__/miscellaneous/file-9.org",
139
+ },
140
+ ],
141
+ }
142
+ `);
143
+ });
144
+
145
+ test('Should detect no changes when all files match stored info', async () => {
146
+ const files = [
147
+ [fn('file-1.org'), new Date('2024-01-01')],
148
+ [fn('file-2.org'), new Date('2024-01-01')],
149
+ [fns('file-3.org'), new Date('2024-01-01')],
150
+ ] as const;
151
+
152
+ files.forEach(([f, d], i) =>
153
+ createTestFile(
154
+ f,
155
+ d ?? new Date('2024-01-01'),
156
+ `:PROPERTIES:
157
+ :ID: ${i + 1}
158
+ :PROPERTIES:
159
+
160
+ * Note:
161
+ - ${f}`
162
+ )
163
+ );
164
+
165
+ const storedNotesInfo: StoredNoteInfo[] = [
166
+ {
167
+ id: '1',
168
+ filePath: fn('file-1.org').split('/'),
169
+ updatedAt: new Date('2024-01-01'),
170
+ },
171
+ {
172
+ id: '2',
173
+ filePath: fn('file-2.org').split('/'),
174
+ updatedAt: new Date('2024-01-01'),
175
+ },
176
+ {
177
+ id: '3',
178
+ filePath: fns('file-3.org').split('/'),
179
+ updatedAt: new Date('2024-01-01'),
180
+ },
181
+ ];
182
+
183
+ const notesFilesDiff = await findNoteFilesDiff({
184
+ fileInfo: async (path: string) => {
185
+ const stats = statSync(path);
186
+ const fileInfo: FileInfo = fileStatToFileInfo(path, stats);
187
+ return Promise.resolve(fileInfo);
188
+ },
189
+ readDir: async (path: string) => {
190
+ const dirents = readdirSync(path, { withFileTypes: true });
191
+ const fileInfos: FileInfo[] = dirents.map((dirent) => {
192
+ const stats = statSync(join(path, dirent.name));
193
+ const fileInfo = fileStatToFileInfo(join(path, dirent.name), stats);
194
+ return fileInfo;
195
+ });
196
+ return Promise.resolve(fileInfos);
197
+ },
198
+ storedNotesInfo,
199
+ dirPath: testFilesFolder,
200
+ });
201
+
202
+ expect(notesFilesDiff).toEqual({
203
+ created: [],
204
+ updated: [],
205
+ deleted: [],
206
+ });
207
+ });
@@ -0,0 +1,18 @@
1
+ import { getFileName, getFileNameWithoutExtension } from '../get-file-name';
2
+ import { test, expect } from 'vitest';
3
+
4
+ test('Should return file name from path', () => {
5
+ expect(getFileName('/some/path/foo.org')).toBe('foo.org');
6
+ });
7
+
8
+ test('Should return file name from file name', () => {
9
+ expect(getFileName('foo.org')).toBe('foo.org');
10
+ });
11
+
12
+ test('Should return file name without extension', () => {
13
+ expect(getFileNameWithoutExtension('foo.org')).toBe('foo');
14
+ });
15
+
16
+ test('Should return file name without extension from path', () => {
17
+ expect(getFileNameWithoutExtension('/some/path/foo.org')).toBe('foo');
18
+ });
@@ -0,0 +1,32 @@
1
+ import { getStringPath } from '../get-string-path';
2
+ import { test, expect } from 'vitest';
3
+
4
+ test('should return the same string if path is a string', () => {
5
+ const path = 'some/path';
6
+ const result = getStringPath(path);
7
+ expect(result).toBe('some/path');
8
+ });
9
+
10
+ test('should join array elements with "/" if path is an array', () => {
11
+ const path = ['some', 'path'];
12
+ const result = getStringPath(path);
13
+ expect(result).toBe('some/path');
14
+ });
15
+
16
+ test('should return an empty string if path is an empty array', () => {
17
+ const path: string[] = [];
18
+ const result = getStringPath(path);
19
+ expect(result).toBe('');
20
+ });
21
+
22
+ test('should handle array with one element correctly', () => {
23
+ const path = ['single'];
24
+ const result = getStringPath(path);
25
+ expect(result).toBe('single');
26
+ });
27
+
28
+ test('should handle array with multiple elements correctly', () => {
29
+ const path = ['this', 'is', 'a', 'test'];
30
+ const result = getStringPath(path);
31
+ expect(result).toBe('this/is/a/test');
32
+ });
@@ -0,0 +1,16 @@
1
+ import { test, expect } from 'vitest';
2
+ import { join } from '../join';
3
+
4
+ test('Should join file paths', () => {
5
+ const samples = [
6
+ ['dir1', 'subdir2', 'file'],
7
+ ['dir2/', '/file'],
8
+ ['/dri3', 'subdir2/', 'file'],
9
+ ['file'],
10
+ ];
11
+
12
+ samples.forEach((sample) => {
13
+ const result = join(...sample);
14
+ expect(result).toMatchSnapshot();
15
+ });
16
+ });
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { readOrgFilesRecursively } from '../find-notes-files-diff';
3
+ import { FileScanParams } from '../../models';
4
+
5
+ // NOTE: AI generated
6
+ const mockReadDir = vi.fn();
7
+ const mockFileInfo = vi.fn();
8
+
9
+ describe('readOrgFilesRecursively', () => {
10
+ it('should read .org files recursively from a directory', async () => {
11
+ const mockFileSystem: FileScanParams = {
12
+ fileInfo: mockFileInfo,
13
+ readDir: mockReadDir,
14
+ dirPath: '/root',
15
+ };
16
+
17
+ // Set up fake file system structure
18
+ mockReadDir.mockImplementation(async (path) => {
19
+ switch (path) {
20
+ case '/root':
21
+ return [
22
+ { name: 'file1.org', type: 'file', size: 100, mtime: Date.now() },
23
+ { name: 'subdir', type: 'directory', size: 0, mtime: Date.now() },
24
+ { name: 'file2.txt', type: 'file', size: 200, mtime: Date.now() },
25
+ ];
26
+ case '/root/subdir':
27
+ return [
28
+ { name: 'file3.org', type: 'file', size: 100, mtime: Date.now() },
29
+ { name: 'file4.md', type: 'file', size: 150, mtime: Date.now() },
30
+ ];
31
+ default:
32
+ return [];
33
+ }
34
+ });
35
+
36
+ // Execute the function
37
+ const orgFiles = await readOrgFilesRecursively(mockFileSystem);
38
+
39
+ // Check results
40
+ expect(orgFiles).toEqual(['/root/file1.org', '/root/subdir/file3.org']);
41
+ expect(mockReadDir).toHaveBeenCalledTimes(2);
42
+ });
43
+
44
+ it('should return an empty array if no .org files are found', async () => {
45
+ const mockFileSystem: FileScanParams = {
46
+ fileInfo: mockFileInfo,
47
+ readDir: mockReadDir,
48
+ dirPath: '/emptyDir',
49
+ };
50
+
51
+ // Set up fake file system structure with no .org files
52
+ mockReadDir.mockResolvedValue([]);
53
+
54
+ // Execute the function
55
+ const orgFiles = await readOrgFilesRecursively(mockFileSystem);
56
+
57
+ // Check results
58
+ expect(orgFiles).toEqual([]);
59
+ expect(mockReadDir).toHaveBeenCalledWith('/emptyDir');
60
+ });
61
+ });
@@ -0,0 +1,35 @@
1
+ import { HandlersCreatingNote, HandlersSyncNotesRequest } from 'src/remote-api';
2
+ import { NoteChange, NoteChanges, FileSystem } from '../models';
3
+ import { isOrgGpgFile } from './is-org-file';
4
+
5
+ export async function extendNotesFilesDiff(
6
+ changes: NoteChanges,
7
+ readFile: FileSystem['readFile']
8
+ ): Promise<HandlersSyncNotesRequest> {
9
+ const upsertedNotesFromLastSync: NoteChange[] = [
10
+ ...changes.updated,
11
+ ...changes.created,
12
+ ];
13
+
14
+ const notesFromLastSync: HandlersCreatingNote[] = [];
15
+
16
+ for (const nc of upsertedNotesFromLastSync) {
17
+ const encrypted = isOrgGpgFile(nc.filePath);
18
+ }
19
+
20
+ const noteIdsFromLastSync = new Set(notesFromLastSync.map((n) => n.id));
21
+
22
+ const deletedNotesIdsWithoutRename = changes.deleted.reduce<string[]>(
23
+ (acc, nc) => {
24
+ if (!noteIdsFromLastSync.has(nc.id)) {
25
+ acc.push(nc.id);
26
+ }
27
+ return acc;
28
+ },
29
+ []
30
+ );
31
+
32
+ return {
33
+ deletedNotesIds: deletedNotesIdsWithoutRename,
34
+ };
35
+ }
@@ -0,0 +1,185 @@
1
+ import {
2
+ SyncParams,
3
+ Changes,
4
+ StoredNoteInfo,
5
+ NoteChanges,
6
+ FileScanParams,
7
+ FileSystem,
8
+ NoteChange,
9
+ } from 'src/models';
10
+ import { getStringPath } from './get-string-path';
11
+ import { isOrgFile } from './is-org-file';
12
+ import { join } from './join';
13
+
14
+ export async function findNoteFilesDiff({
15
+ fileInfo,
16
+ readDir,
17
+ lastSync,
18
+ storedNotesInfo,
19
+ dirPath,
20
+ }: SyncParams): Promise<NoteChanges> {
21
+ const orgFilePaths = await readOrgFilesRecursively({
22
+ fileInfo,
23
+ readDir,
24
+ dirPath,
25
+ });
26
+
27
+ const filesFromLastSync = await getOrgFilesFromLastSync(
28
+ orgFilePaths,
29
+ fileInfo,
30
+ lastSync
31
+ );
32
+
33
+ const deleted = findDeletedNotes(orgFilePaths, storedNotesInfo);
34
+
35
+ const [created, updated] = await findUpdatedCreatedNotes(
36
+ fileInfo,
37
+ filesFromLastSync,
38
+ storedNotesInfo
39
+ );
40
+
41
+ return {
42
+ deleted,
43
+ created,
44
+ updated,
45
+ };
46
+ }
47
+
48
+ export async function getOrgFilesFromLastSync(
49
+ filePaths: string[],
50
+ fileInfo: FileSystem['fileInfo'],
51
+ lastSync?: Date
52
+ ): Promise<string[]> {
53
+ if (!lastSync) {
54
+ return filePaths;
55
+ }
56
+
57
+ return Promise.all(
58
+ filePaths.filter(async (filePath) => {
59
+ const stats = await fileInfo(filePath);
60
+ return (
61
+ new Date(stats.mtime) > lastSync || new Date(stats.ctime) > lastSync
62
+ );
63
+ })
64
+ );
65
+ }
66
+
67
+ function findDeletedNotes(
68
+ filePaths: string[],
69
+ storedNotesInfo: StoredNoteInfo[]
70
+ ): NoteChange[] {
71
+ const uniqueFilePaths = new Set(filePaths);
72
+
73
+ return storedNotesInfo.reduce((acc, storedNote) => {
74
+ const storedNotePath = getStringPath(storedNote.filePath);
75
+ if (uniqueFilePaths.has(storedNotePath)) {
76
+ return acc;
77
+ }
78
+ acc.push({
79
+ filePath: storedNotePath,
80
+ id: storedNote.id,
81
+ });
82
+ return acc;
83
+ }, []);
84
+ }
85
+
86
+ async function findUpdatedCreatedNotes(
87
+ fileInfo: FileSystem['fileInfo'],
88
+ orgFilePaths: string[],
89
+ storedNotesInfo: StoredNoteInfo[]
90
+ ): Promise<[NoteChange[], NoteChange[]]> {
91
+ const created: NoteChange[] = [];
92
+ const updated: NoteChange[] = [];
93
+
94
+ const storedNotesInfoMap = getStoredNotesInfoMap(storedNotesInfo);
95
+
96
+ for (const f of orgFilePaths) {
97
+ const found = storedNotesInfoMap[f];
98
+ if (!found) {
99
+ created.push({ filePath: f });
100
+ continue;
101
+ }
102
+ const fileUpdatedTime = new Date((await fileInfo(f))?.mtime);
103
+ if (found.updatedAt < fileUpdatedTime) {
104
+ updated.push({ filePath: f });
105
+ }
106
+ }
107
+
108
+ return [created, updated];
109
+ }
110
+
111
+ function getStoredNotesInfoMap(storedNotesInfo: StoredNoteInfo[]): {
112
+ [key: string]: StoredNoteInfo;
113
+ } {
114
+ return storedNotesInfo.reduce((acc, n) => {
115
+ acc[getStringPath(n.filePath)] = n;
116
+ return acc;
117
+ }, {});
118
+ }
119
+
120
+ export async function readOrgFilesRecursively({
121
+ fileInfo,
122
+ readDir,
123
+ dirPath,
124
+ }: FileScanParams): Promise<string[]> {
125
+ const files = await readDir(dirPath);
126
+
127
+ const collectedPaths: string[] = [];
128
+ const getFullPath = (p: string) => join(dirPath, p);
129
+
130
+ for (const f of files) {
131
+ if (f.type === 'directory') {
132
+ const subDirFiles = await readOrgFilesRecursively({
133
+ fileInfo,
134
+ readDir,
135
+ dirPath: getFullPath(f.name),
136
+ });
137
+
138
+ collectedPaths.push(...subDirFiles);
139
+ continue;
140
+ }
141
+
142
+ if (!isOrgFile(f.name)) {
143
+ continue;
144
+ }
145
+
146
+ collectedPaths.push(getFullPath(f.name));
147
+ }
148
+
149
+ return collectedPaths;
150
+ }
151
+
152
+ export function findFilesDiff(
153
+ filePaths: string[],
154
+ storedNotesInfo: StoredNoteInfo[],
155
+ updatedTimeGetter: (filePath: string) => Date
156
+ ): Changes {
157
+ const existingFilePaths = new Set<string>(filePaths);
158
+
159
+ const storedNotesPathSet = new Set<string>(
160
+ storedNotesInfo.map((n) => getStringPath(n.filePath))
161
+ );
162
+ const changedFiles: Changes = { deleted: [], created: [], updated: [] };
163
+
164
+ storedNotesInfo.forEach((storedNote) => {
165
+ const fullPath = getStringPath(storedNote.filePath);
166
+ if (!existingFilePaths.has(fullPath)) {
167
+ changedFiles.deleted.push(fullPath);
168
+ return;
169
+ }
170
+
171
+ const updatedTime = updatedTimeGetter(fullPath);
172
+ if (updatedTime > new Date(storedNote.updatedAt)) {
173
+ changedFiles.updated.push(fullPath);
174
+ }
175
+ });
176
+
177
+ filePaths.forEach((filePath) => {
178
+ if (!storedNotesPathSet.has(filePath)) {
179
+ changedFiles.created.push(filePath);
180
+ return;
181
+ }
182
+ });
183
+
184
+ return changedFiles;
185
+ }
@@ -0,0 +1,7 @@
1
+ export const getFileName = (path: string): string => {
2
+ return path.split('/').pop();
3
+ };
4
+
5
+ export const getFileNameWithoutExtension = (path: string): string => {
6
+ return getFileName(path).split('.').shift();
7
+ };
@@ -0,0 +1,6 @@
1
+ export function getStringPath(path: string | string[]): string {
2
+ if (Array.isArray(path)) {
3
+ return `${path.join('/')}`;
4
+ }
5
+ return path;
6
+ }
package/tools/index.ts CHANGED
@@ -1,3 +1,6 @@
1
1
  export * from './mock-server';
2
2
  export * from './is-gpg-encrypted';
3
3
  export * from './is-org-file';
4
+ export * from './get-string-path';
5
+ export * from './find-notes-files-diff';
6
+ export * from './join';
package/tools/join.ts ADDED
@@ -0,0 +1,3 @@
1
+ export function join(...path: string[]): string {
2
+ return path.join('/').replace(/\/+/g, '/').replace(/\/+$/, '');
3
+ }