sliftutils 1.7.4 → 1.7.6
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/index.d.ts +281 -2
- package/misc/https/persistentLocalStorage.ts +2 -0
- package/package.json +1 -1
- package/render-utils/Input.tsx +2 -0
- package/render-utils/InputLabel.tsx +2 -0
- package/render-utils/colors.tsx +2 -0
- package/storage/BulkDatabase2/syncClient.ts +10 -7
- package/storage/DiskCollection.d.ts +1 -0
- package/storage/DiskCollection.ts +3 -1
- package/storage/FileFolderAPI.d.ts +1 -0
- package/storage/FileFolderAPI.tsx +18 -0
- package/storage/IArchives.d.ts +37 -0
- package/storage/IArchives.ts +20 -0
- package/storage/TransactionStorage.d.ts +2 -1
- package/storage/TransactionStorage.ts +6 -2
- package/storage/backblaze.d.ts +2 -1
- package/storage/backblaze.ts +2 -1
- package/storage/remoteStorage/ArchivesRemote.d.ts +67 -0
- package/storage/remoteStorage/ArchivesRemote.ts +195 -0
- package/storage/remoteStorage/accessPage.d.ts +1 -0
- package/storage/remoteStorage/accessPage.tsx +133 -0
- package/storage/remoteStorage/blobStore.d.ts +56 -0
- package/storage/remoteStorage/blobStore.ts +403 -0
- package/storage/remoteStorage/storageController.d.ts +89 -0
- package/storage/remoteStorage/storageController.ts +398 -0
- package/storage/remoteStorage/storageServer.d.ts +1 -0
- package/storage/remoteStorage/storageServer.ts +132 -0
- package/teststorage/browser.tsx +148 -0
- package/teststorage/server.ts +43 -0
- package/yarn.lock +3 -3
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { lazy } from "socket-function/src/caching";
|
|
4
|
+
import { runInSerial, runInfinitePoll } from "socket-function/src/batching";
|
|
5
|
+
import { timeInMinute, sort } from "socket-function/src/misc";
|
|
6
|
+
import { formatNumber } from "socket-function/src/formatting/format";
|
|
7
|
+
import { TransactionStorage } from "../TransactionStorage";
|
|
8
|
+
import { JSONStorage } from "../JSONStorage";
|
|
9
|
+
import { getFileStorageNested2 } from "../FileFolderAPI";
|
|
10
|
+
import { ArchiveFileInfo } from "../IArchives";
|
|
11
|
+
|
|
12
|
+
// Disk engine for the remote storage server. Files are packed into large append-only blob files
|
|
13
|
+
// (instead of one file on disk per stored file), with a transaction-log index mapping
|
|
14
|
+
// key -> (blob, offset, length). This keeps us efficient with many small files and scales to
|
|
15
|
+
// terabytes: reads are single pread calls, writes are appends, and deleted space is reclaimed by
|
|
16
|
+
// compacting blobs that are mostly dead.
|
|
17
|
+
|
|
18
|
+
// Roll to a new blob file once the current one reaches this size
|
|
19
|
+
const MAX_BLOB_SIZE = 4 * 1024 * 1024 * 1024;
|
|
20
|
+
// Whole-file memory cache (only files up to MEMORY_CACHE_MAX_FILE are cached)
|
|
21
|
+
const MEMORY_CACHE_BYTES = 256 * 1024 * 1024;
|
|
22
|
+
const MEMORY_CACHE_MAX_FILE = 16 * 1024 * 1024;
|
|
23
|
+
export const DEFAULT_FAST_WRITE_DELAY = timeInMinute * 5;
|
|
24
|
+
const FAST_FLUSH_POLL = 1000 * 15;
|
|
25
|
+
const COMPACTION_INTERVAL = timeInMinute * 10;
|
|
26
|
+
// Compact a blob once this fraction of its bytes are dead (deleted/overwritten)
|
|
27
|
+
const COMPACTION_DEAD_FRACTION = 0.5;
|
|
28
|
+
const MAX_OPEN_BLOBS = 64;
|
|
29
|
+
|
|
30
|
+
type IndexEntry = {
|
|
31
|
+
// Blob file name
|
|
32
|
+
f: string;
|
|
33
|
+
// Offset + length within the blob
|
|
34
|
+
o: number;
|
|
35
|
+
l: number;
|
|
36
|
+
// Write time
|
|
37
|
+
t: number;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type WriteConfig = {
|
|
41
|
+
// Resolve once the write is in memory; flush to disk after writeDelay, coalescing writes to
|
|
42
|
+
// the same key (only the latest is written). Data is lost if the process crashes first.
|
|
43
|
+
fast?: boolean;
|
|
44
|
+
writeDelay?: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type OverlayEntry = {
|
|
48
|
+
// undefined data means a pending delete
|
|
49
|
+
data: Buffer | undefined;
|
|
50
|
+
t: number;
|
|
51
|
+
flushAt: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
class ByteLRU {
|
|
55
|
+
private map = new Map<string, Buffer>();
|
|
56
|
+
private bytes = 0;
|
|
57
|
+
constructor(private budget: number, private maxEntry: number) { }
|
|
58
|
+
public get(key: string): Buffer | undefined {
|
|
59
|
+
let value = this.map.get(key);
|
|
60
|
+
if (value) {
|
|
61
|
+
this.map.delete(key);
|
|
62
|
+
this.map.set(key, value);
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
public set(key: string, value: Buffer) {
|
|
67
|
+
this.delete(key);
|
|
68
|
+
if (value.length > this.maxEntry) return;
|
|
69
|
+
this.map.set(key, value);
|
|
70
|
+
this.bytes += value.length;
|
|
71
|
+
while (this.bytes > this.budget && this.map.size > 0) {
|
|
72
|
+
let oldest = this.map.keys().next().value;
|
|
73
|
+
if (oldest === undefined) break;
|
|
74
|
+
this.delete(oldest);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
public delete(key: string) {
|
|
78
|
+
let existing = this.map.get(key);
|
|
79
|
+
if (!existing) return;
|
|
80
|
+
this.bytes -= existing.length;
|
|
81
|
+
this.map.delete(key);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class BlobStore {
|
|
86
|
+
constructor(private folder: string) { }
|
|
87
|
+
|
|
88
|
+
private memCache = new ByteLRU(MEMORY_CACHE_BYTES, MEMORY_CACHE_MAX_FILE);
|
|
89
|
+
private overlay = new Map<string, OverlayEntry>();
|
|
90
|
+
private writeQueue = runInSerial(async (fnc: () => Promise<void>) => fnc());
|
|
91
|
+
private openBlobs = new Map<string, Promise<fs.promises.FileHandle>>();
|
|
92
|
+
private largeUploads = new Map<string, { fd: fs.promises.FileHandle; tmpPath: string; size: number }>();
|
|
93
|
+
private nextLargeUploadId = 1;
|
|
94
|
+
|
|
95
|
+
private currentBlobNumber = 0;
|
|
96
|
+
private currentBlobOffset = 0;
|
|
97
|
+
private currentBlobFd: fs.promises.FileHandle | undefined;
|
|
98
|
+
|
|
99
|
+
private index!: JSONStorage<IndexEntry>;
|
|
100
|
+
// Dead (deleted/overwritten) byte count per blob file, for compaction
|
|
101
|
+
private deadBytes!: JSONStorage<number>;
|
|
102
|
+
|
|
103
|
+
private blobsDir = path.join(this.folder, "blobs");
|
|
104
|
+
|
|
105
|
+
public init = lazy(async () => {
|
|
106
|
+
fs.mkdirSync(this.blobsDir, { recursive: true });
|
|
107
|
+
let root = await getFileStorageNested2(path.resolve(this.folder));
|
|
108
|
+
let indexRaw = await root.folder.getStorage("index");
|
|
109
|
+
this.index = new JSONStorage<IndexEntry>(new TransactionStorage(indexRaw, "blobStoreIndex"));
|
|
110
|
+
let metaRaw = await root.folder.getStorage("meta");
|
|
111
|
+
this.deadBytes = new JSONStorage<number>(new TransactionStorage(metaRaw, "blobStoreDeadBytes"));
|
|
112
|
+
|
|
113
|
+
for (let file of fs.readdirSync(this.blobsDir)) {
|
|
114
|
+
let match = /^blob_(\d+)\.bin$/.exec(file);
|
|
115
|
+
if (!match) continue;
|
|
116
|
+
this.currentBlobNumber = Math.max(this.currentBlobNumber, +match[1]);
|
|
117
|
+
}
|
|
118
|
+
if (this.currentBlobNumber > 0) {
|
|
119
|
+
this.currentBlobOffset = fs.statSync(path.join(this.blobsDir, this.blobName(this.currentBlobNumber))).size;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
runInfinitePoll(FAST_FLUSH_POLL, () => this.flushOverlay());
|
|
123
|
+
runInfinitePoll(COMPACTION_INTERVAL, () => this.compact());
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
private blobName(n: number) {
|
|
127
|
+
return `blob_${String(n).padStart(6, "0")}.bin`;
|
|
128
|
+
}
|
|
129
|
+
private blobPath(name: string) {
|
|
130
|
+
return path.join(this.blobsDir, name);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private async getBlobHandle(name: string): Promise<fs.promises.FileHandle> {
|
|
134
|
+
let cached = this.openBlobs.get(name);
|
|
135
|
+
if (cached) {
|
|
136
|
+
// Re-insert for LRU ordering
|
|
137
|
+
this.openBlobs.delete(name);
|
|
138
|
+
this.openBlobs.set(name, cached);
|
|
139
|
+
return cached;
|
|
140
|
+
}
|
|
141
|
+
let handle = fs.promises.open(this.blobPath(name), "r");
|
|
142
|
+
this.openBlobs.set(name, handle);
|
|
143
|
+
while (this.openBlobs.size > MAX_OPEN_BLOBS) {
|
|
144
|
+
let oldest = this.openBlobs.keys().next().value;
|
|
145
|
+
if (oldest === undefined) break;
|
|
146
|
+
let oldHandle = this.openBlobs.get(oldest);
|
|
147
|
+
this.openBlobs.delete(oldest);
|
|
148
|
+
void oldHandle?.then(h => h.close()).catch(() => { });
|
|
149
|
+
}
|
|
150
|
+
return handle;
|
|
151
|
+
}
|
|
152
|
+
private async closeBlobHandle(name: string) {
|
|
153
|
+
let handle = this.openBlobs.get(name);
|
|
154
|
+
if (!handle) return;
|
|
155
|
+
this.openBlobs.delete(name);
|
|
156
|
+
await handle.then(h => h.close()).catch(() => { });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async addDeadBytes(entry: IndexEntry) {
|
|
160
|
+
// Large files get a dedicated blob file, so on delete we can unlink it immediately
|
|
161
|
+
if (entry.f.startsWith("large_")) {
|
|
162
|
+
await this.closeBlobHandle(entry.f);
|
|
163
|
+
await fs.promises.unlink(this.blobPath(entry.f)).catch(() => { });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
let dead = await this.deadBytes.get(entry.f) || 0;
|
|
167
|
+
await this.deadBytes.set(entry.f, dead + entry.l);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Appends data to the current blob file, returning where it landed
|
|
171
|
+
private async appendData(data: Buffer): Promise<{ f: string; o: number; l: number }> {
|
|
172
|
+
let result: { f: string; o: number; l: number } | undefined;
|
|
173
|
+
await this.writeQueue(async () => {
|
|
174
|
+
if (!this.currentBlobFd || this.currentBlobOffset >= MAX_BLOB_SIZE) {
|
|
175
|
+
if (this.currentBlobFd) {
|
|
176
|
+
await this.currentBlobFd.close();
|
|
177
|
+
this.currentBlobFd = undefined;
|
|
178
|
+
}
|
|
179
|
+
this.currentBlobNumber++;
|
|
180
|
+
this.currentBlobOffset = 0;
|
|
181
|
+
}
|
|
182
|
+
if (!this.currentBlobFd) {
|
|
183
|
+
this.currentBlobFd = await fs.promises.open(this.blobPath(this.blobName(this.currentBlobNumber)), "a");
|
|
184
|
+
this.currentBlobOffset = (await this.currentBlobFd.stat()).size;
|
|
185
|
+
}
|
|
186
|
+
let offset = this.currentBlobOffset;
|
|
187
|
+
await this.currentBlobFd.write(data, 0, data.length);
|
|
188
|
+
this.currentBlobOffset += data.length;
|
|
189
|
+
result = { f: this.blobName(this.currentBlobNumber), o: offset, l: data.length };
|
|
190
|
+
});
|
|
191
|
+
if (!result) throw new Error(`Append did not run, this should be impossible`);
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private async setIndexEntry(key: string, entry: IndexEntry) {
|
|
196
|
+
let prev = await this.index.get(key);
|
|
197
|
+
await this.index.set(key, entry);
|
|
198
|
+
if (prev) {
|
|
199
|
+
await this.addDeadBytes(prev);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
public async set(key: string, data: Buffer, config?: WriteConfig): Promise<void> {
|
|
204
|
+
await this.init();
|
|
205
|
+
this.memCache.set(key, data);
|
|
206
|
+
if (config?.fast) {
|
|
207
|
+
let writeDelay = config.writeDelay || DEFAULT_FAST_WRITE_DELAY;
|
|
208
|
+
this.overlay.set(key, { data, t: Date.now(), flushAt: Date.now() + writeDelay });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
this.overlay.delete(key);
|
|
212
|
+
let location = await this.appendData(data);
|
|
213
|
+
await this.setIndexEntry(key, { ...location, t: Date.now() });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
public async del(key: string, config?: WriteConfig): Promise<void> {
|
|
217
|
+
await this.init();
|
|
218
|
+
this.memCache.delete(key);
|
|
219
|
+
if (config?.fast) {
|
|
220
|
+
let writeDelay = config.writeDelay || DEFAULT_FAST_WRITE_DELAY;
|
|
221
|
+
this.overlay.set(key, { data: undefined, t: Date.now(), flushAt: Date.now() + writeDelay });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
this.overlay.delete(key);
|
|
225
|
+
let prev = await this.index.get(key);
|
|
226
|
+
if (!prev) return;
|
|
227
|
+
await this.index.remove(key);
|
|
228
|
+
await this.addDeadBytes(prev);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public async get(key: string, range?: { start: number; end: number }): Promise<Buffer | undefined> {
|
|
232
|
+
await this.init();
|
|
233
|
+
let overlayEntry = this.overlay.get(key);
|
|
234
|
+
if (overlayEntry) {
|
|
235
|
+
if (!overlayEntry.data) return undefined;
|
|
236
|
+
if (!range) return overlayEntry.data;
|
|
237
|
+
return overlayEntry.data.subarray(Math.min(range.start, overlayEntry.data.length), Math.min(range.end, overlayEntry.data.length));
|
|
238
|
+
}
|
|
239
|
+
let cached = this.memCache.get(key);
|
|
240
|
+
if (cached) {
|
|
241
|
+
if (!range) return cached;
|
|
242
|
+
return cached.subarray(Math.min(range.start, cached.length), Math.min(range.end, cached.length));
|
|
243
|
+
}
|
|
244
|
+
let entry = await this.index.get(key);
|
|
245
|
+
if (!entry) return undefined;
|
|
246
|
+
let start = range && Math.min(range.start, entry.l) || 0;
|
|
247
|
+
let end = range && Math.min(range.end, entry.l) || entry.l;
|
|
248
|
+
if (end <= start) return Buffer.alloc(0);
|
|
249
|
+
let handle = await this.getBlobHandle(entry.f);
|
|
250
|
+
let buffer = Buffer.alloc(end - start);
|
|
251
|
+
let { bytesRead } = await handle.read(buffer, 0, buffer.length, entry.o + start);
|
|
252
|
+
if (bytesRead !== buffer.length) {
|
|
253
|
+
throw new Error(`Expected ${buffer.length} bytes at ${entry.f}:${entry.o + start} for ${key}, read ${bytesRead}`);
|
|
254
|
+
}
|
|
255
|
+
if (!range && buffer.length <= MEMORY_CACHE_MAX_FILE) {
|
|
256
|
+
this.memCache.set(key, buffer);
|
|
257
|
+
}
|
|
258
|
+
return buffer;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
public async getInfo(key: string): Promise<{ writeTime: number; size: number } | undefined> {
|
|
262
|
+
await this.init();
|
|
263
|
+
let overlayEntry = this.overlay.get(key);
|
|
264
|
+
if (overlayEntry) {
|
|
265
|
+
if (!overlayEntry.data) return undefined;
|
|
266
|
+
return { writeTime: overlayEntry.t, size: overlayEntry.data.length };
|
|
267
|
+
}
|
|
268
|
+
let entry = await this.index.get(key);
|
|
269
|
+
if (!entry) return undefined;
|
|
270
|
+
return { writeTime: entry.t, size: entry.l };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
public async findInfo(prefix: string, config?: { shallow?: boolean; type?: "files" | "folders" }): Promise<ArchiveFileInfo[]> {
|
|
274
|
+
await this.init();
|
|
275
|
+
let infos = new Map<string, ArchiveFileInfo>();
|
|
276
|
+
for (let key of await this.index.getKeys()) {
|
|
277
|
+
if (!key.startsWith(prefix)) continue;
|
|
278
|
+
if (this.overlay.has(key)) continue;
|
|
279
|
+
let entry = await this.index.get(key);
|
|
280
|
+
if (!entry) continue;
|
|
281
|
+
infos.set(key, { path: key, createTime: entry.t, size: entry.l });
|
|
282
|
+
}
|
|
283
|
+
for (let [key, overlayEntry] of this.overlay) {
|
|
284
|
+
if (!key.startsWith(prefix)) continue;
|
|
285
|
+
if (!overlayEntry.data) continue;
|
|
286
|
+
infos.set(key, { path: key, createTime: overlayEntry.t, size: overlayEntry.data.length });
|
|
287
|
+
}
|
|
288
|
+
let files = Array.from(infos.values());
|
|
289
|
+
if (config?.type === "folders") {
|
|
290
|
+
let folders = new Map<string, ArchiveFileInfo>();
|
|
291
|
+
for (let file of files) {
|
|
292
|
+
let rest = file.path.slice(prefix.length);
|
|
293
|
+
let restParts = rest.split("/");
|
|
294
|
+
if (restParts.length < 2) continue;
|
|
295
|
+
let folder: string;
|
|
296
|
+
if (config.shallow) {
|
|
297
|
+
folder = prefix + restParts[0];
|
|
298
|
+
} else {
|
|
299
|
+
folder = file.path.split("/").slice(0, -1).join("/");
|
|
300
|
+
}
|
|
301
|
+
folders.set(folder, { path: folder, createTime: file.createTime, size: file.size });
|
|
302
|
+
}
|
|
303
|
+
files = Array.from(folders.values());
|
|
304
|
+
} else if (config?.shallow) {
|
|
305
|
+
files = files.filter(file => !file.path.slice(prefix.length).includes("/"));
|
|
306
|
+
}
|
|
307
|
+
sort(files, x => x.path);
|
|
308
|
+
return files;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Large files stream into their own dedicated blob file, so concurrent small writes don't
|
|
312
|
+
// interleave into the middle of them.
|
|
313
|
+
public async startLargeUpload(): Promise<string> {
|
|
314
|
+
await this.init();
|
|
315
|
+
let id = `${Date.now()}_${this.nextLargeUploadId++}`;
|
|
316
|
+
let tmpPath = path.join(this.blobsDir, `upload_${id}.tmp`);
|
|
317
|
+
let fd = await fs.promises.open(tmpPath, "w");
|
|
318
|
+
this.largeUploads.set(id, { fd, tmpPath, size: 0 });
|
|
319
|
+
return id;
|
|
320
|
+
}
|
|
321
|
+
public async appendLargeUpload(id: string, data: Buffer): Promise<void> {
|
|
322
|
+
let upload = this.largeUploads.get(id);
|
|
323
|
+
if (!upload) throw new Error(`Unknown large upload ${id}`);
|
|
324
|
+
await upload.fd.write(data, 0, data.length);
|
|
325
|
+
upload.size += data.length;
|
|
326
|
+
}
|
|
327
|
+
public async finishLargeUpload(id: string, key: string): Promise<void> {
|
|
328
|
+
let upload = this.largeUploads.get(id);
|
|
329
|
+
if (!upload) throw new Error(`Unknown large upload ${id}`);
|
|
330
|
+
this.largeUploads.delete(id);
|
|
331
|
+
await upload.fd.close();
|
|
332
|
+
let blobName = `large_${id}.bin`;
|
|
333
|
+
await fs.promises.rename(upload.tmpPath, this.blobPath(blobName));
|
|
334
|
+
this.memCache.delete(key);
|
|
335
|
+
this.overlay.delete(key);
|
|
336
|
+
await this.setIndexEntry(key, { f: blobName, o: 0, l: upload.size, t: Date.now() });
|
|
337
|
+
}
|
|
338
|
+
public async cancelLargeUpload(id: string): Promise<void> {
|
|
339
|
+
let upload = this.largeUploads.get(id);
|
|
340
|
+
if (!upload) return;
|
|
341
|
+
this.largeUploads.delete(id);
|
|
342
|
+
await upload.fd.close();
|
|
343
|
+
await fs.promises.unlink(upload.tmpPath).catch(() => { });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private async flushOverlay(): Promise<void> {
|
|
347
|
+
let now = Date.now();
|
|
348
|
+
for (let [key, entry] of this.overlay) {
|
|
349
|
+
if (entry.flushAt > now) continue;
|
|
350
|
+
if (entry.data) {
|
|
351
|
+
let location = await this.appendData(entry.data);
|
|
352
|
+
await this.setIndexEntry(key, { ...location, t: entry.t });
|
|
353
|
+
} else {
|
|
354
|
+
let prev = await this.index.get(key);
|
|
355
|
+
if (prev) {
|
|
356
|
+
await this.index.remove(key);
|
|
357
|
+
await this.addDeadBytes(prev);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Only remove if it wasn't overwritten while we were flushing
|
|
361
|
+
if (this.overlay.get(key) === entry) {
|
|
362
|
+
this.overlay.delete(key);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private async compact(): Promise<void> {
|
|
368
|
+
let currentBlob = this.blobName(this.currentBlobNumber);
|
|
369
|
+
for (let blobName of await this.deadBytes.getKeys()) {
|
|
370
|
+
if (blobName === currentBlob) continue;
|
|
371
|
+
let dead = await this.deadBytes.get(blobName) || 0;
|
|
372
|
+
let blobPath = this.blobPath(blobName);
|
|
373
|
+
let size = 0;
|
|
374
|
+
try {
|
|
375
|
+
size = fs.statSync(blobPath).size;
|
|
376
|
+
} catch {
|
|
377
|
+
await this.deadBytes.remove(blobName);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (dead < size * COMPACTION_DEAD_FRACTION) continue;
|
|
381
|
+
|
|
382
|
+
console.log(`Compacting blob ${blobName} (${formatNumber(dead)}B dead of ${formatNumber(size)}B)`);
|
|
383
|
+
for (let key of await this.index.getKeys()) {
|
|
384
|
+
let entry = await this.index.get(key);
|
|
385
|
+
if (!entry || entry.f !== blobName) continue;
|
|
386
|
+
let data = await this.get(key);
|
|
387
|
+
if (!data) continue;
|
|
388
|
+
let location = await this.appendData(data);
|
|
389
|
+
// Only move it if it wasn't rewritten while we were reading
|
|
390
|
+
let latest = await this.index.get(key);
|
|
391
|
+
if (latest && latest.f === entry.f && latest.o === entry.o) {
|
|
392
|
+
await this.index.set(key, { ...location, t: latest.t });
|
|
393
|
+
} else {
|
|
394
|
+
// Our copy is stale, so its bytes are immediately dead
|
|
395
|
+
await this.deadBytes.set(location.f, (await this.deadBytes.get(location.f) || 0) + location.l);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
await this.closeBlobHandle(blobName);
|
|
399
|
+
await fs.promises.unlink(blobPath).catch(() => { });
|
|
400
|
+
await this.deadBytes.remove(blobName);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
import { ArchiveFileInfo } from "../IArchives";
|
|
4
|
+
import type { BlobStore } from "./blobStore";
|
|
5
|
+
import type { IStorage } from "../IStorage";
|
|
6
|
+
export declare const REMOTE_STORAGE_CLASS_GUID = "RemoteStorageController-b7e42a91";
|
|
7
|
+
export declare const STORAGE_AUTH_PURPOSE = "remoteStorage-auth-1";
|
|
8
|
+
export declare const STORAGE_NOT_AUTHENTICATED = "REMOTE_STORAGE_NOT_AUTHENTICATED_cf2f7b1e";
|
|
9
|
+
export declare const STORAGE_ACCESS_DENIED = "REMOTE_STORAGE_ACCESS_DENIED_9d81a4c0";
|
|
10
|
+
export type AuthToken = {
|
|
11
|
+
certPem: string;
|
|
12
|
+
time: number;
|
|
13
|
+
signature: string;
|
|
14
|
+
};
|
|
15
|
+
export type AccessRequest = {
|
|
16
|
+
requestId: string;
|
|
17
|
+
account: string;
|
|
18
|
+
machineId: string;
|
|
19
|
+
ip: string;
|
|
20
|
+
time: number;
|
|
21
|
+
};
|
|
22
|
+
export type TrustRecord = {
|
|
23
|
+
account: string;
|
|
24
|
+
machineId: string;
|
|
25
|
+
ip: string;
|
|
26
|
+
time: number;
|
|
27
|
+
};
|
|
28
|
+
export type BucketConfig = {
|
|
29
|
+
public?: boolean;
|
|
30
|
+
fast?: boolean;
|
|
31
|
+
writeDelay?: number;
|
|
32
|
+
};
|
|
33
|
+
export type AccessState = {
|
|
34
|
+
machineId: string;
|
|
35
|
+
ip: string;
|
|
36
|
+
hasAccess: boolean;
|
|
37
|
+
listAccessCommand: string;
|
|
38
|
+
grantAccessCommand?: string;
|
|
39
|
+
machines?: (AccessRequest & {
|
|
40
|
+
trusted: boolean;
|
|
41
|
+
})[];
|
|
42
|
+
};
|
|
43
|
+
export type StorageServerState = {
|
|
44
|
+
domain: string;
|
|
45
|
+
port: number;
|
|
46
|
+
rootDomain: string;
|
|
47
|
+
sshTarget: string;
|
|
48
|
+
serverCommand: string;
|
|
49
|
+
blobStore: BlobStore;
|
|
50
|
+
trust: IStorage<TrustRecord>;
|
|
51
|
+
requests: IStorage<AccessRequest[]>;
|
|
52
|
+
buckets: IStorage<BucketConfig>;
|
|
53
|
+
};
|
|
54
|
+
export declare function setStorageServerState(state: StorageServerState): void;
|
|
55
|
+
export declare const RemoteStorageController: import("socket-function/SocketFunctionTypes").SocketRegistered<{
|
|
56
|
+
authenticate: (token: AuthToken) => Promise<{
|
|
57
|
+
machineId: string;
|
|
58
|
+
ip: string;
|
|
59
|
+
}>;
|
|
60
|
+
requestAccess: (account: string) => Promise<{
|
|
61
|
+
machineId: string;
|
|
62
|
+
ip: string;
|
|
63
|
+
requestId: string;
|
|
64
|
+
grantAccessCommand: string;
|
|
65
|
+
}>;
|
|
66
|
+
getAccessState: (account: string) => Promise<AccessState>;
|
|
67
|
+
adminListRequests: (ip: string) => Promise<AccessRequest[]>;
|
|
68
|
+
adminGrantAccess: (requestId: string) => Promise<TrustRecord>;
|
|
69
|
+
ensureBucket: (account: string, bucketName: string, config: BucketConfig) => Promise<void>;
|
|
70
|
+
get: (account: string, bucketName: string, path: string, range?: {
|
|
71
|
+
start: number;
|
|
72
|
+
end: number;
|
|
73
|
+
}) => Promise<Buffer | undefined>;
|
|
74
|
+
set: (account: string, bucketName: string, path: string, data: Buffer) => Promise<void>;
|
|
75
|
+
del: (account: string, bucketName: string, path: string) => Promise<void>;
|
|
76
|
+
getInfo: (account: string, bucketName: string, path: string) => Promise<{
|
|
77
|
+
writeTime: number;
|
|
78
|
+
size: number;
|
|
79
|
+
} | undefined>;
|
|
80
|
+
findInfo: (account: string, bucketName: string, prefix: string, config?: {
|
|
81
|
+
shallow?: boolean;
|
|
82
|
+
type?: "files" | "folders";
|
|
83
|
+
}) => Promise<ArchiveFileInfo[]>;
|
|
84
|
+
startLargeFile: (account: string, bucketName: string, path: string) => Promise<string>;
|
|
85
|
+
uploadPart: (uploadId: string, data: Buffer) => Promise<void>;
|
|
86
|
+
finishLargeFile: (uploadId: string) => Promise<void>;
|
|
87
|
+
cancelLargeFile: (uploadId: string) => Promise<void>;
|
|
88
|
+
getPublicFile: (account: string, bucketName: string, path: string) => Promise<Buffer>;
|
|
89
|
+
}>;
|