orgnote-api 0.17.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.
- package/encryption/__tests__/__snapshots__/note-encryption.spec.ts.snap +0 -16
- package/encryption/__tests__/encryption.spec.ts +5 -2
- package/encryption/__tests__/note-encryption.spec.ts +0 -1
- package/encryption/encryption.ts +1 -0
- package/encryption/note-encryption.ts +2 -1
- package/models/encryption.ts +7 -3
- package/models/file-system.ts +30 -0
- package/models/index.ts +2 -0
- package/models/sync.ts +43 -0
- package/package.json +1 -1
- package/tools/__tests__/__snapshots__/join.spec.ts.snap +9 -0
- package/tools/__tests__/{find-notes-files-diff.spec.ts → find-files-diff.spec.ts} +9 -8
- package/tools/__tests__/find-note-files-diff.spec.ts +207 -0
- package/tools/__tests__/get-file-name.spec.ts +18 -0
- package/tools/__tests__/join.spec.ts +16 -0
- package/tools/__tests__/read-org-files-recursively.spec.ts +61 -0
- package/tools/extend-notes-files-diff.ts +35 -0
- package/tools/find-notes-files-diff.ts +148 -11
- package/tools/get-file-name.ts +7 -0
- package/tools/index.ts +3 -0
- package/tools/join.ts +3 -0
|
@@ -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
|
-
|
|
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,7 @@ test('Should armor and unarmor encrypted file', async () => {
|
|
|
325
328
|
"
|
|
326
329
|
`);
|
|
327
330
|
|
|
328
|
-
const {
|
|
331
|
+
const { data } = await unarmor(armored);
|
|
329
332
|
|
|
330
333
|
expect(data).toEqual(content);
|
|
331
334
|
});
|
package/encryption/encryption.ts
CHANGED
|
@@ -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
|
-
):
|
|
50
|
+
): DecryptionResult<T> {
|
|
50
51
|
const isContentEncrypted = isGpgEncrypted(encryptionParams.content);
|
|
51
52
|
if (
|
|
52
53
|
note.meta.published ||
|
package/models/encryption.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface OrgNoteGpgEncryption {
|
|
|
15
15
|
/* Armored private key */
|
|
16
16
|
privateKey: string;
|
|
17
17
|
/* Armored public key */
|
|
18
|
-
publicKey
|
|
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 &
|
|
50
|
+
> = T & DecriptionData & BaseOrgNoteDecryption;
|
|
47
51
|
|
|
48
52
|
export type WithNoteDecryptionContent<
|
|
49
53
|
T extends OrgNoteEncryption = OrgNoteEncryption,
|
|
50
|
-
> = T &
|
|
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
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
|
@@ -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"`;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { afterEach, beforeEach, expect, test } from 'vitest';
|
|
2
2
|
import { mkdirSync, rmdirSync, statSync, utimesSync, writeFileSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import {
|
|
4
|
+
import { findFilesDiff } from '../find-notes-files-diff';
|
|
5
|
+
import { StoredNoteInfo } from '../../models';
|
|
5
6
|
|
|
6
|
-
const testFilesFolder = 'src/tools/__tests__/
|
|
7
|
+
const testFilesFolder = 'src/tools/__tests__/miscellaneous2/';
|
|
7
8
|
|
|
8
9
|
function initFiles(): void {
|
|
9
10
|
mkdirSync(testFilesFolder);
|
|
@@ -73,7 +74,7 @@ test('Should find files diff', () => {
|
|
|
73
74
|
},
|
|
74
75
|
];
|
|
75
76
|
|
|
76
|
-
const changedFiles =
|
|
77
|
+
const changedFiles = findFilesDiff(
|
|
77
78
|
[
|
|
78
79
|
testFilesFolder + 'org-file.org',
|
|
79
80
|
testFilesFolder + 'org-file2.org',
|
|
@@ -118,7 +119,7 @@ test('Should find files diff when folder was renamed', () => {
|
|
|
118
119
|
|
|
119
120
|
rmdirSync(testFilesFolder + 'nested-folder', { recursive: true });
|
|
120
121
|
|
|
121
|
-
const changedFiles =
|
|
122
|
+
const changedFiles = findFilesDiff(
|
|
122
123
|
[],
|
|
123
124
|
storedNoteInfos,
|
|
124
125
|
(filePath: string) => new Date(statSync(filePath).mtime)
|
|
@@ -128,8 +129,8 @@ test('Should find files diff when folder was renamed', () => {
|
|
|
128
129
|
{
|
|
129
130
|
"created": [],
|
|
130
131
|
"deleted": [
|
|
131
|
-
"src/tools/__tests__/
|
|
132
|
-
"src/tools/__tests__/
|
|
132
|
+
"src/tools/__tests__/miscellaneous2/nested-folder/org-file.org",
|
|
133
|
+
"src/tools/__tests__/miscellaneous2/nested-folder/org-file2.org",
|
|
133
134
|
],
|
|
134
135
|
"updated": [],
|
|
135
136
|
}
|
|
@@ -146,7 +147,7 @@ test('Should find created note when nested folder created', () => {
|
|
|
146
147
|
|
|
147
148
|
const storedNoteInfos: StoredNoteInfo[] = [];
|
|
148
149
|
|
|
149
|
-
const changedFiles =
|
|
150
|
+
const changedFiles = findFilesDiff(
|
|
150
151
|
[testFilesFolder + 'new-nested-folder/org-file.org'],
|
|
151
152
|
storedNoteInfos,
|
|
152
153
|
(filePath: string) => new Date(statSync(filePath).mtime)
|
|
@@ -162,7 +163,7 @@ test('Should find created note when nested folder created', () => {
|
|
|
162
163
|
test('Should find nothing when no files provided', () => {
|
|
163
164
|
const storedNoteInfos: StoredNoteInfo[] = [];
|
|
164
165
|
|
|
165
|
-
const changedFiles =
|
|
166
|
+
const changedFiles = findFilesDiff(
|
|
166
167
|
[],
|
|
167
168
|
storedNoteInfos,
|
|
168
169
|
(filePath: string) => new Date(statSync(filePath).mtime)
|
|
@@ -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,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
|
+
}
|
|
@@ -1,28 +1,165 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SyncParams,
|
|
3
|
+
Changes,
|
|
4
|
+
StoredNoteInfo,
|
|
5
|
+
NoteChanges,
|
|
6
|
+
FileScanParams,
|
|
7
|
+
FileSystem,
|
|
8
|
+
NoteChange,
|
|
9
|
+
} from 'src/models';
|
|
1
10
|
import { getStringPath } from './get-string-path';
|
|
11
|
+
import { isOrgFile } from './is-org-file';
|
|
12
|
+
import { join } from './join';
|
|
2
13
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
+
}, []);
|
|
7
84
|
}
|
|
8
85
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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;
|
|
13
150
|
}
|
|
14
151
|
|
|
15
|
-
export function
|
|
152
|
+
export function findFilesDiff(
|
|
16
153
|
filePaths: string[],
|
|
17
154
|
storedNotesInfo: StoredNoteInfo[],
|
|
18
155
|
updatedTimeGetter: (filePath: string) => Date
|
|
19
|
-
):
|
|
156
|
+
): Changes {
|
|
20
157
|
const existingFilePaths = new Set<string>(filePaths);
|
|
21
158
|
|
|
22
159
|
const storedNotesPathSet = new Set<string>(
|
|
23
160
|
storedNotesInfo.map((n) => getStringPath(n.filePath))
|
|
24
161
|
);
|
|
25
|
-
const changedFiles:
|
|
162
|
+
const changedFiles: Changes = { deleted: [], created: [], updated: [] };
|
|
26
163
|
|
|
27
164
|
storedNotesInfo.forEach((storedNote) => {
|
|
28
165
|
const fullPath = getStringPath(storedNote.filePath);
|
package/tools/index.ts
CHANGED
package/tools/join.ts
ADDED