@axium/storage 0.7.10 → 0.9.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/dist/{client.d.ts → client/api.d.ts} +2 -2
- package/dist/{client.js → client/api.js} +21 -6
- package/dist/client/cli.d.ts +1 -0
- package/dist/client/cli.js +194 -0
- package/dist/client/config.d.ts +17 -0
- package/dist/client/config.js +20 -0
- package/dist/client/hooks.d.ts +1 -0
- package/dist/client/hooks.js +6 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/local.d.ts +58 -0
- package/dist/client/local.js +90 -0
- package/dist/client/sync.d.ts +67 -0
- package/dist/client/sync.js +196 -0
- package/dist/common.d.ts +120 -21
- package/dist/common.js +39 -1
- package/dist/node.d.ts +13 -0
- package/dist/node.js +52 -0
- package/dist/polyfills.d.ts +1 -1
- package/dist/polyfills.js +3 -3
- package/dist/server/api.d.ts +1 -0
- package/dist/server/api.js +187 -0
- package/dist/server/batch.d.ts +1 -0
- package/dist/server/batch.js +147 -0
- package/dist/server/cli.d.ts +1 -0
- package/dist/server/cli.js +14 -0
- package/dist/{server.d.ts → server/config.d.ts} +12 -52
- package/dist/server/config.js +48 -0
- package/dist/server/db.d.ts +48 -0
- package/dist/server/db.js +81 -0
- package/dist/{hooks.d.ts → server/hooks.d.ts} +3 -2
- package/dist/{hooks.js → server/hooks.js} +7 -3
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +5 -0
- package/dist/server/raw.d.ts +1 -0
- package/dist/server/raw.js +163 -0
- package/lib/List.svelte +1 -1
- package/lib/Usage.svelte +4 -4
- package/package.json +15 -8
- package/routes/files/[id]/+page.svelte +9 -5
- package/routes/files/usage/+page.svelte +3 -5
- package/dist/server.js +0 -412
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StorageItemMetadata, StorageItemUpdate, UserStorage, UserStorageInfo } from '
|
|
1
|
+
import type { StorageItemMetadata, StorageItemUpdate, UserStorage, UserStorageInfo } from '../common.js';
|
|
2
2
|
export declare function parseItem(result: StorageItemMetadata): StorageItemMetadata;
|
|
3
3
|
export interface UploadOptions {
|
|
4
4
|
parentId?: string;
|
|
@@ -15,7 +15,7 @@ export declare function downloadItem(fileId: string): Promise<Blob>;
|
|
|
15
15
|
export declare function updateItemMetadata(fileId: string, metadata: StorageItemUpdate): Promise<StorageItemMetadata>;
|
|
16
16
|
export declare function deleteItem(fileId: string): Promise<StorageItemMetadata>;
|
|
17
17
|
export declare function getUserStorage(userId: string): Promise<UserStorage>;
|
|
18
|
-
export declare function
|
|
18
|
+
export declare function getUserStats(userId: string): Promise<UserStorageInfo>;
|
|
19
19
|
export declare function getUserTrash(userId: string): Promise<StorageItemMetadata[]>;
|
|
20
20
|
export declare function itemsSharedWith(userId: string): Promise<StorageItemMetadata[]>;
|
|
21
21
|
export declare function getUserStorageRoot(userId: string): Promise<StorageItemMetadata[]>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fetchAPI, token } from '@axium/client/requests';
|
|
1
|
+
import { fetchAPI, prefix, token } from '@axium/client/requests';
|
|
2
2
|
async function _upload(method, url, data, extraHeaders = {}) {
|
|
3
3
|
const init = {
|
|
4
4
|
method,
|
|
@@ -30,16 +30,24 @@ export function parseItem(result) {
|
|
|
30
30
|
result.trashedAt = new Date(result.trashedAt);
|
|
31
31
|
return result;
|
|
32
32
|
}
|
|
33
|
+
function rawStorage(fileId) {
|
|
34
|
+
const raw = '/raw/storage' + (fileId ? '/' + fileId : '');
|
|
35
|
+
if (prefix[0] == '/')
|
|
36
|
+
return raw;
|
|
37
|
+
const url = new URL(prefix);
|
|
38
|
+
url.pathname = raw;
|
|
39
|
+
return url;
|
|
40
|
+
}
|
|
33
41
|
export async function uploadItem(file, opt = {}) {
|
|
34
42
|
const headers = {};
|
|
35
43
|
if (opt.parentId)
|
|
36
44
|
headers['x-parent'] = opt.parentId;
|
|
37
45
|
if (opt.name)
|
|
38
46
|
headers['x-name'] = opt.name;
|
|
39
|
-
return parseItem(await _upload('PUT',
|
|
47
|
+
return parseItem(await _upload('PUT', rawStorage(), file, headers));
|
|
40
48
|
}
|
|
41
49
|
export async function updateItem(fileId, data) {
|
|
42
|
-
return parseItem(await _upload('POST',
|
|
50
|
+
return parseItem(await _upload('POST', rawStorage(fileId), data));
|
|
43
51
|
}
|
|
44
52
|
export async function getItemMetadata(fileId) {
|
|
45
53
|
const result = await fetchAPI('GET', 'storage/item/:id', undefined, fileId);
|
|
@@ -55,7 +63,7 @@ export async function getDirectoryMetadata(parentId) {
|
|
|
55
63
|
return result;
|
|
56
64
|
}
|
|
57
65
|
export async function downloadItem(fileId) {
|
|
58
|
-
const response = await fetch(
|
|
66
|
+
const response = await fetch(rawStorage(fileId), {
|
|
59
67
|
headers: token ? { Authorization: 'Bearer ' + token } : {},
|
|
60
68
|
});
|
|
61
69
|
if (!response.ok)
|
|
@@ -72,12 +80,19 @@ export async function deleteItem(fileId) {
|
|
|
72
80
|
}
|
|
73
81
|
export async function getUserStorage(userId) {
|
|
74
82
|
const result = await fetchAPI('GET', 'users/:id/storage', undefined, userId);
|
|
83
|
+
result.lastModified = new Date(result.lastModified);
|
|
84
|
+
if (result.lastTrashed)
|
|
85
|
+
result.lastTrashed = new Date(result.lastTrashed);
|
|
75
86
|
for (const item of result.items)
|
|
76
87
|
parseItem(item);
|
|
77
88
|
return result;
|
|
78
89
|
}
|
|
79
|
-
export async function
|
|
80
|
-
|
|
90
|
+
export async function getUserStats(userId) {
|
|
91
|
+
const result = await fetchAPI('OPTIONS', 'users/:id/storage', undefined, userId);
|
|
92
|
+
result.lastModified = new Date(result.lastModified);
|
|
93
|
+
if (result.lastTrashed)
|
|
94
|
+
result.lastTrashed = new Date(result.lastTrashed);
|
|
95
|
+
return result;
|
|
81
96
|
}
|
|
82
97
|
export async function getUserTrash(userId) {
|
|
83
98
|
const result = await fetchAPI('GET', 'users/:id/storage/trash', undefined, userId);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { configDir, session } from '@axium/client/cli/config';
|
|
2
|
+
import { formatBytes } from '@axium/core/format';
|
|
3
|
+
import * as io from '@axium/core/node/io';
|
|
4
|
+
import { Option, program } from 'commander';
|
|
5
|
+
import { statSync, unlinkSync } from 'node:fs';
|
|
6
|
+
import { stat } from 'node:fs/promises';
|
|
7
|
+
import { join, resolve } from 'node:path';
|
|
8
|
+
import { styleText } from 'node:util';
|
|
9
|
+
import { colorItem, formatItems } from '../node.js';
|
|
10
|
+
import * as api from './api.js';
|
|
11
|
+
import { config, saveConfig } from './config.js';
|
|
12
|
+
import { cachePath, getDirectory, resolveItem, setQuiet, syncCache } from './local.js';
|
|
13
|
+
import { computeDelta, doSync, fetchSyncItems } from './sync.js';
|
|
14
|
+
const cli = program
|
|
15
|
+
.command('files')
|
|
16
|
+
.helpGroup('Plugins:')
|
|
17
|
+
.description('CLI integration for @axium/storage')
|
|
18
|
+
.option('-q, --quiet', 'Suppress output')
|
|
19
|
+
.hook('preAction', (action) => {
|
|
20
|
+
const opts = action.optsWithGlobals();
|
|
21
|
+
if (opts.quiet)
|
|
22
|
+
setQuiet(true);
|
|
23
|
+
});
|
|
24
|
+
cli.command('usage')
|
|
25
|
+
.description('Show your usage')
|
|
26
|
+
.action(async () => {
|
|
27
|
+
const { limits, itemCount, usedBytes } = await api.getUserStats(session().userId);
|
|
28
|
+
console.log(`Items: ${itemCount} ${limits.user_items ? ' / ' + limits.user_items : ''}`);
|
|
29
|
+
console.log(`Space: ${formatBytes(usedBytes)} ${limits.user_size ? ' / ' + formatBytes(limits.user_size * 1_000_000) : ''}`);
|
|
30
|
+
});
|
|
31
|
+
cli.command('ls')
|
|
32
|
+
.alias('list')
|
|
33
|
+
.description('List the contents of a folder')
|
|
34
|
+
.argument('<path>', 'remote folder path')
|
|
35
|
+
.option('-l, --long', 'Show more details')
|
|
36
|
+
.option('-h, --human-readable', 'Show sizes in human readable format')
|
|
37
|
+
.action(async function (path) {
|
|
38
|
+
const { users } = await syncCache().catch(io.handleError);
|
|
39
|
+
const { long, humanReadable } = this.optsWithGlobals();
|
|
40
|
+
const items = await getDirectory(path).catch(io.handleError);
|
|
41
|
+
if (!long) {
|
|
42
|
+
console.log(items.map(colorItem).join('\t'));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
console.log('total ' + items.length);
|
|
46
|
+
for (const text of formatItems({ items, users, humanReadable }))
|
|
47
|
+
console.log(text);
|
|
48
|
+
});
|
|
49
|
+
cli.command('mkdir')
|
|
50
|
+
.description('Create a remote folder')
|
|
51
|
+
.argument('<path>', 'remote folder path to create')
|
|
52
|
+
.action(async (path) => {
|
|
53
|
+
const pathParts = path.split('/');
|
|
54
|
+
const name = pathParts.pop();
|
|
55
|
+
const parentPath = pathParts.join('/');
|
|
56
|
+
const parent = !parentPath ? null : await resolveItem(parentPath).catch(io.handleError);
|
|
57
|
+
if (parent) {
|
|
58
|
+
if (!parent)
|
|
59
|
+
io.exit('Could not resolve parent folder.');
|
|
60
|
+
if (parent.type != 'inode/directory')
|
|
61
|
+
io.exit('Parent path is not a directory.');
|
|
62
|
+
}
|
|
63
|
+
await api.uploadItem(new Blob([], { type: 'inode/directory' }), { parentId: parent?.id, name }).catch(io.handleError);
|
|
64
|
+
});
|
|
65
|
+
cli.command('status')
|
|
66
|
+
.option('-v, --verbose', 'Show more details')
|
|
67
|
+
.option('--refresh', 'Force refresh metadata from the server')
|
|
68
|
+
.action(async (opt) => {
|
|
69
|
+
console.log(styleText('bold', `${config.sync.length} synced folder(s):`));
|
|
70
|
+
for (const sync of config.sync) {
|
|
71
|
+
if (opt.refresh)
|
|
72
|
+
await fetchSyncItems(sync.item, sync.name);
|
|
73
|
+
const delta = computeDelta(sync);
|
|
74
|
+
if (opt.verbose) {
|
|
75
|
+
console.log(styleText('underline', sync.local_path + ':'));
|
|
76
|
+
if (delta.synced.length == delta.items.length && !delta.local_only.length) {
|
|
77
|
+
console.log('\t' + styleText('blueBright', 'All files are synced!'));
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
for (const { _path } of delta.local_only)
|
|
81
|
+
console.log('\t' + styleText('green', '+ ' + _path));
|
|
82
|
+
for (const { path } of delta.remote_only)
|
|
83
|
+
console.log('\t' + styleText('red', '- ' + path));
|
|
84
|
+
for (const { path, modifiedAt } of delta.modified) {
|
|
85
|
+
const outdated = modifiedAt.getTime() > statSync(join(sync.local_path, path)).mtime.getTime();
|
|
86
|
+
console.log('\t' + styleText('yellow', '~ ' + path) + (outdated ? ' (outdated)' : ''));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
console.log(sync.local_path + ':', Object.entries(delta)
|
|
91
|
+
.map(([name, items]) => `${styleText('blueBright', items.length.toString())} ${name.replaceAll('_', '-')}`)
|
|
92
|
+
.join(', '));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
cli.command('add')
|
|
97
|
+
.description('Add a folder to be synced')
|
|
98
|
+
.argument('<path>', 'local path to the folder to sync')
|
|
99
|
+
.argument('<remote>', 'remote folder path')
|
|
100
|
+
.action(async (localPath, remoteName) => {
|
|
101
|
+
localPath = resolve(localPath);
|
|
102
|
+
for (const sync of config.sync) {
|
|
103
|
+
if (sync.local_path == localPath || localPath.startsWith(sync.local_path + '/'))
|
|
104
|
+
io.exit('This local path is already being synced.');
|
|
105
|
+
if (sync.remote_path == remoteName || remoteName.startsWith(sync.remote_path + '/'))
|
|
106
|
+
io.exit('This remote path is already being synced.');
|
|
107
|
+
}
|
|
108
|
+
const local = await stat(localPath).catch(e => io.exit(e.toString()));
|
|
109
|
+
if (!local.isDirectory())
|
|
110
|
+
io.exit('Local path is not a directory.');
|
|
111
|
+
const remote = await resolveItem(remoteName);
|
|
112
|
+
if (!remote)
|
|
113
|
+
io.exit('Could not resolve remote path.');
|
|
114
|
+
if (remote.type != 'inode/directory')
|
|
115
|
+
io.exit('Remote path is not a directory.');
|
|
116
|
+
config.sync.push({
|
|
117
|
+
name: remote.name,
|
|
118
|
+
item: remote.id,
|
|
119
|
+
local_path: localPath,
|
|
120
|
+
last_synced: new Date(),
|
|
121
|
+
remote_path: remoteName,
|
|
122
|
+
include_dotfiles: false,
|
|
123
|
+
exclude: [],
|
|
124
|
+
});
|
|
125
|
+
await fetchSyncItems(remote.id);
|
|
126
|
+
saveConfig();
|
|
127
|
+
});
|
|
128
|
+
cli.command('unsync')
|
|
129
|
+
.alias('remove-sync')
|
|
130
|
+
.alias('rm-sync')
|
|
131
|
+
.description('Stop syncing a folder')
|
|
132
|
+
.argument('<path>', 'local path to the folder to stop syncing')
|
|
133
|
+
.action((localPath) => {
|
|
134
|
+
localPath = resolve(localPath);
|
|
135
|
+
const index = config.sync.findIndex(sync => sync.local_path == localPath);
|
|
136
|
+
if (index == -1)
|
|
137
|
+
io.exit('This local path is not being synced.');
|
|
138
|
+
unlinkSync(join(configDir, 'sync', config.sync[index].item + '.json'));
|
|
139
|
+
config.sync.splice(index, 1);
|
|
140
|
+
saveConfig();
|
|
141
|
+
});
|
|
142
|
+
cli.command('sync')
|
|
143
|
+
.description('Sync files')
|
|
144
|
+
.addOption(new Option('--delete <mode>', 'Delete local/remote files that were deleted remotely/locally')
|
|
145
|
+
.choices(['local', 'remote', 'none'])
|
|
146
|
+
.default('none'))
|
|
147
|
+
.option('-d, --dry-run', 'Show what would be done, but do not make any changes')
|
|
148
|
+
.option('-v, --verbose', 'Show more details')
|
|
149
|
+
.argument('[sync]', 'The name of the Sync to sync')
|
|
150
|
+
.action(async (name, opt) => {
|
|
151
|
+
if (name) {
|
|
152
|
+
const sync = config.sync.find(s => s.name == name);
|
|
153
|
+
if (!sync)
|
|
154
|
+
io.exit('Can not find a Sync with that name.');
|
|
155
|
+
await doSync(sync, opt);
|
|
156
|
+
}
|
|
157
|
+
else
|
|
158
|
+
for (const sync of config.sync)
|
|
159
|
+
await doSync(sync, opt);
|
|
160
|
+
});
|
|
161
|
+
const cliCache = cli.command('cache').description('Manage the local cache');
|
|
162
|
+
cliCache
|
|
163
|
+
.command('clear')
|
|
164
|
+
.description('Clear the local cache')
|
|
165
|
+
.action(() => unlinkSync(cachePath));
|
|
166
|
+
cliCache
|
|
167
|
+
.command('refresh')
|
|
168
|
+
.description('Force a refresh of the local cache from the server')
|
|
169
|
+
.action(async () => {
|
|
170
|
+
await syncCache(true).catch(io.handleError);
|
|
171
|
+
});
|
|
172
|
+
cliCache
|
|
173
|
+
.command('dump')
|
|
174
|
+
.description('Dump the local cache')
|
|
175
|
+
.option('-v, --verbose', 'Show more details')
|
|
176
|
+
.addOption(new Option('-j, --json', 'Output as JSON').conflicts(['verbose', 'quiet']))
|
|
177
|
+
.action(async function () {
|
|
178
|
+
const opt = this.optsWithGlobals();
|
|
179
|
+
const data = await syncCache(false).catch(io.handleError);
|
|
180
|
+
if (opt.json) {
|
|
181
|
+
console.log(JSON.stringify(data));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
console.log(`Cache contains ${data.items.length} items and ${Object.keys(data.users).length} users.`);
|
|
185
|
+
if (opt.quiet || !opt.verbose)
|
|
186
|
+
return;
|
|
187
|
+
console.log(styleText('bold', 'Items:'));
|
|
188
|
+
for (const text of formatItems({ ...data, humanReadable: true }))
|
|
189
|
+
console.log(text);
|
|
190
|
+
console.log(styleText('bold', 'Users:'));
|
|
191
|
+
for (const user of Object.values(data.users)) {
|
|
192
|
+
console.log(user.name, `<${user.email}>`, styleText('dim', `(${user.id})`));
|
|
193
|
+
}
|
|
194
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
declare const ClientStorageConfig: z.ZodObject<{
|
|
3
|
+
sync: z.ZodArray<z.ZodObject<{
|
|
4
|
+
name: z.ZodString;
|
|
5
|
+
item: z.ZodUUID;
|
|
6
|
+
local_path: z.ZodString;
|
|
7
|
+
last_synced: z.ZodCoercedDate<unknown>;
|
|
8
|
+
remote_path: z.ZodString;
|
|
9
|
+
include_dotfiles: z.ZodDefault<z.ZodBoolean>;
|
|
10
|
+
exclude: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
11
|
+
}, z.core.$loose>>;
|
|
12
|
+
}, z.core.$loose>;
|
|
13
|
+
export interface ClientStorageConfig extends z.infer<typeof ClientStorageConfig> {
|
|
14
|
+
}
|
|
15
|
+
export declare let config: ClientStorageConfig;
|
|
16
|
+
export declare function saveConfig(): void;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { configDir } from '@axium/client/cli/config';
|
|
2
|
+
import { debug, readJSON, writeJSON } from '@axium/core/node/io';
|
|
3
|
+
import { join } from 'node:path/posix';
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
import { Sync } from './sync.js';
|
|
6
|
+
const ClientStorageConfig = z.looseObject({
|
|
7
|
+
sync: Sync.array(),
|
|
8
|
+
});
|
|
9
|
+
const configPath = join(configDir, 'storage.json');
|
|
10
|
+
export let config = { sync: [] };
|
|
11
|
+
try {
|
|
12
|
+
config = readJSON(configPath, ClientStorageConfig);
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
debug('Could not load @axium/storage config: ' + e);
|
|
16
|
+
}
|
|
17
|
+
export function saveConfig() {
|
|
18
|
+
writeJSON(configPath, config);
|
|
19
|
+
debug('Saved @axium/storage config.');
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function run(): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './api.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './api.js';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { UserPublic } from '@axium/core';
|
|
2
|
+
import * as z from 'zod';
|
|
3
|
+
import { StorageItemMetadata } from '../common.js';
|
|
4
|
+
export declare function setQuiet(enabled: boolean): void;
|
|
5
|
+
export declare function walkItems(path: string, items: StorageItemMetadata[]): StorageItemMetadata | null;
|
|
6
|
+
declare const StorageCache: z.ZodObject<{
|
|
7
|
+
items: z.ZodArray<z.ZodObject<{
|
|
8
|
+
createdAt: z.ZodCoercedDate<unknown>;
|
|
9
|
+
dataURL: z.ZodString;
|
|
10
|
+
hash: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
11
|
+
id: z.ZodUUID;
|
|
12
|
+
immutable: z.ZodBoolean;
|
|
13
|
+
modifiedAt: z.ZodCoercedDate<unknown>;
|
|
14
|
+
name: z.ZodString;
|
|
15
|
+
userId: z.ZodUUID;
|
|
16
|
+
parentId: z.ZodNullable<z.ZodUUID>;
|
|
17
|
+
publicPermission: z.ZodEnum<{
|
|
18
|
+
readonly None: 0;
|
|
19
|
+
readonly Read: 1;
|
|
20
|
+
readonly Comment: 2;
|
|
21
|
+
readonly Edit: 3;
|
|
22
|
+
readonly Manage: 5;
|
|
23
|
+
}> & {
|
|
24
|
+
readonly None: 0;
|
|
25
|
+
readonly Read: 1;
|
|
26
|
+
readonly Comment: 2;
|
|
27
|
+
readonly Edit: 3;
|
|
28
|
+
readonly Manage: 5;
|
|
29
|
+
};
|
|
30
|
+
size: z.ZodInt;
|
|
31
|
+
trashedAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
|
|
32
|
+
type: z.ZodString;
|
|
33
|
+
metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
34
|
+
}, z.core.$strip>>;
|
|
35
|
+
users: z.ZodRecord<z.ZodUUID, z.ZodObject<{
|
|
36
|
+
id: z.ZodUUID;
|
|
37
|
+
name: z.ZodString;
|
|
38
|
+
email: z.ZodOptional<z.ZodEmail>;
|
|
39
|
+
emailVerified: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodDate>>>;
|
|
40
|
+
image: z.ZodOptional<z.ZodNullable<z.ZodURL>>;
|
|
41
|
+
preferences: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
|
|
42
|
+
roles: z.ZodArray<z.ZodString>;
|
|
43
|
+
registeredAt: z.ZodCoercedDate<unknown>;
|
|
44
|
+
isAdmin: z.ZodOptional<z.ZodBoolean>;
|
|
45
|
+
}, z.core.$strip>>;
|
|
46
|
+
}, z.core.$strip>;
|
|
47
|
+
interface StorageCache extends z.infer<typeof StorageCache> {
|
|
48
|
+
users: Record<string, UserPublic>;
|
|
49
|
+
}
|
|
50
|
+
export declare const cachePath: string;
|
|
51
|
+
export declare let cachedData: StorageCache | null;
|
|
52
|
+
/**
|
|
53
|
+
* @param force true => always refresh, false => never refresh, null (default) => refresh if outdated
|
|
54
|
+
*/
|
|
55
|
+
export declare function syncCache(force?: boolean | null): Promise<StorageCache>;
|
|
56
|
+
export declare function resolveItem(path: string): Promise<StorageItemMetadata | null>;
|
|
57
|
+
export declare function getDirectory(path: string): Promise<StorageItemMetadata[]>;
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { cacheDir, session } from '@axium/client/cli/config';
|
|
2
|
+
import { userInfo } from '@axium/client/user';
|
|
3
|
+
import { UserPublic } from '@axium/core';
|
|
4
|
+
import * as io from '@axium/core/node/io';
|
|
5
|
+
import { ENOENT, ENOTDIR } from 'node:constants';
|
|
6
|
+
import { stat } from 'node:fs/promises';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import * as z from 'zod';
|
|
9
|
+
import { StorageItemMetadata } from '../common.js';
|
|
10
|
+
import { getUserStats, getUserStorage } from './api.js';
|
|
11
|
+
let quiet = false;
|
|
12
|
+
export function setQuiet(enabled) {
|
|
13
|
+
quiet = enabled;
|
|
14
|
+
}
|
|
15
|
+
export function walkItems(path, items) {
|
|
16
|
+
const resolved = [];
|
|
17
|
+
let currentItem = null, currentParentId = null;
|
|
18
|
+
const target = path.split('/').filter(p => p);
|
|
19
|
+
for (const part of target) {
|
|
20
|
+
for (const item of items) {
|
|
21
|
+
if (item.parentId != currentParentId || item.name != part)
|
|
22
|
+
continue;
|
|
23
|
+
currentItem = item;
|
|
24
|
+
currentParentId = item.id;
|
|
25
|
+
resolved.push(part);
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return currentItem;
|
|
30
|
+
}
|
|
31
|
+
const StorageCache = z.object({
|
|
32
|
+
items: StorageItemMetadata.array(),
|
|
33
|
+
users: z.record(z.uuid(), UserPublic),
|
|
34
|
+
});
|
|
35
|
+
export const cachePath = join(cacheDir, 'storage.json');
|
|
36
|
+
export let cachedData = null;
|
|
37
|
+
/**
|
|
38
|
+
* @param force true => always refresh, false => never refresh, null (default) => refresh if outdated
|
|
39
|
+
*/
|
|
40
|
+
export async function syncCache(force = null) {
|
|
41
|
+
if (cachedData)
|
|
42
|
+
return cachedData;
|
|
43
|
+
const cacheUpdated = await stat(cachePath)
|
|
44
|
+
.then(stats => stats.mtimeMs)
|
|
45
|
+
.catch(() => 0);
|
|
46
|
+
const { userId } = session();
|
|
47
|
+
let { lastModified, lastTrashed } = await getUserStats(userId);
|
|
48
|
+
lastTrashed ??= new Date(0);
|
|
49
|
+
if ((force || cacheUpdated < lastModified.getTime() || cacheUpdated < lastTrashed.getTime()) && force !== false) {
|
|
50
|
+
if (!quiet)
|
|
51
|
+
io.start('Updating and loading item metadata');
|
|
52
|
+
try {
|
|
53
|
+
const { items } = await getUserStorage(userId);
|
|
54
|
+
const users = {};
|
|
55
|
+
for (const item of items) {
|
|
56
|
+
users[item.userId] ||= await userInfo(item.userId);
|
|
57
|
+
}
|
|
58
|
+
cachedData = { items, users };
|
|
59
|
+
io.writeJSON(cachePath, cachedData);
|
|
60
|
+
if (!quiet)
|
|
61
|
+
io.done();
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
65
|
+
if (quiet)
|
|
66
|
+
io.exit('Failed to update item metadata: ' + message);
|
|
67
|
+
else
|
|
68
|
+
io.exit(message);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
cachedData = io.readJSON(cachePath, StorageCache);
|
|
73
|
+
}
|
|
74
|
+
return cachedData;
|
|
75
|
+
}
|
|
76
|
+
export async function resolveItem(path) {
|
|
77
|
+
const { items } = await syncCache();
|
|
78
|
+
return walkItems(path, items);
|
|
79
|
+
}
|
|
80
|
+
export async function getDirectory(path) {
|
|
81
|
+
const { items } = await syncCache();
|
|
82
|
+
if (path == '/' || path == '')
|
|
83
|
+
return items.filter(item => item.parentId === null);
|
|
84
|
+
const dir = walkItems(path, items);
|
|
85
|
+
if (!dir)
|
|
86
|
+
throw ENOENT;
|
|
87
|
+
if (dir.type != 'inode/directory')
|
|
88
|
+
throw ENOTDIR;
|
|
89
|
+
return items.filter(item => item.parentId === dir.id);
|
|
90
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as z from 'zod';
|
|
3
|
+
import '../polyfills.js';
|
|
4
|
+
/**
|
|
5
|
+
* A Sync is a storage item that has been selected for synchronization by the user.
|
|
6
|
+
* Importantly, it is *only* the "top-level" item.
|
|
7
|
+
* This means if a user selects a directory to sync, only that directory is a Sync.
|
|
8
|
+
*
|
|
9
|
+
* `Sync`s are client-side in nature, the server does not care about them.
|
|
10
|
+
* (we could do something fancy server-side at a later date)
|
|
11
|
+
*/
|
|
12
|
+
export declare const Sync: z.ZodObject<{
|
|
13
|
+
name: z.ZodString;
|
|
14
|
+
item: z.ZodUUID;
|
|
15
|
+
local_path: z.ZodString;
|
|
16
|
+
last_synced: z.ZodCoercedDate<unknown>;
|
|
17
|
+
remote_path: z.ZodString;
|
|
18
|
+
include_dotfiles: z.ZodDefault<z.ZodBoolean>;
|
|
19
|
+
exclude: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
20
|
+
}, z.core.$loose>;
|
|
21
|
+
export interface Sync extends z.infer<typeof Sync> {
|
|
22
|
+
}
|
|
23
|
+
/** Local metadata about a storage item to sync */
|
|
24
|
+
export declare const LocalItem: z.ZodObject<{
|
|
25
|
+
id: z.ZodUUID;
|
|
26
|
+
path: z.ZodString;
|
|
27
|
+
modifiedAt: z.ZodCoercedDate<unknown>;
|
|
28
|
+
hash: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
|
|
29
|
+
}, z.core.$strip>;
|
|
30
|
+
export interface LocalItem extends z.infer<typeof LocalItem> {
|
|
31
|
+
}
|
|
32
|
+
export declare function fetchSyncItems(id: string, folderName?: string): Promise<LocalItem[]>;
|
|
33
|
+
export declare function getItems(id: string): LocalItem[];
|
|
34
|
+
export declare function setItems(id: string, items: LocalItem[]): void;
|
|
35
|
+
/** Metadata about a synced storage item. */
|
|
36
|
+
export interface ItemMetadata {
|
|
37
|
+
}
|
|
38
|
+
/** Computed metadata about changes */
|
|
39
|
+
export interface Delta {
|
|
40
|
+
synced: LocalItem[];
|
|
41
|
+
modified: LocalItem[];
|
|
42
|
+
remote_only: LocalItem[];
|
|
43
|
+
items: LocalItem[];
|
|
44
|
+
_items: Map<string, LocalItem>;
|
|
45
|
+
local_only: (fs.Dirent<string> & {
|
|
46
|
+
_path: string;
|
|
47
|
+
})[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Computes the changes between the local and remote, in the direction of the remote.
|
|
51
|
+
*/
|
|
52
|
+
export declare function computeDelta(sync: Sync): Delta;
|
|
53
|
+
/**
|
|
54
|
+
* A helper to run an action on an array, with support for dry-runs and displaying a progress counter or listing each item.
|
|
55
|
+
*/
|
|
56
|
+
export declare function applyAction<T>(opt: {
|
|
57
|
+
dryRun: boolean;
|
|
58
|
+
verbose: boolean;
|
|
59
|
+
}, items: T[], label: (item: T) => string, action: (item: T) => Promise<void | string> | void | string): Promise<void>;
|
|
60
|
+
export interface SyncOptions {
|
|
61
|
+
delete: 'local' | 'remote' | 'none';
|
|
62
|
+
dryRun: boolean;
|
|
63
|
+
verbose: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface SyncStats {
|
|
66
|
+
}
|
|
67
|
+
export declare function doSync(sync: Sync, opt: SyncOptions): Promise<SyncStats>;
|