sliftutils 1.7.9 → 1.7.11
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/bin/storageserver.js +4 -0
- package/{teststorage → examplestorage}/browser.tsx +18 -22
- package/{teststorage/server.ts → examplestorage/exampleserver.ts} +4 -6
- package/index.d.ts +770 -84
- package/misc/dist/environment.ts.cache +3 -3
- package/misc/dist/getSecret.ts.cache +139 -0
- package/misc/dist/strings.ts.cache +13 -0
- package/misc/dist/zip.ts.cache +9 -0
- package/misc/getSecret.ts +38 -16
- package/misc/https/cloudflareHelpers.d.ts +19 -0
- package/misc/https/cloudflareHelpers.ts +69 -0
- package/misc/https/dist/certs.ts.cache +115 -179
- package/misc/https/dist/cloudflareHelpers.ts.cache +68 -0
- package/misc/https/dist/dns.ts.cache +112 -100
- package/misc/https/dist/hostServer.ts.cache +130 -0
- package/misc/https/dist/httpsCerts.ts.cache +24 -5
- package/misc/https/dist/persistentLocalStorage.ts.cache +3 -2
- package/misc/https/dns.d.ts +41 -8
- package/misc/https/dns.ts +104 -106
- package/misc/https/hostServer.d.ts +0 -3
- package/misc/https/hostServer.ts +12 -8
- package/package.json +3 -2
- package/render-utils/dist/observer.tsx.cache +3 -2
- package/spec.txt +0 -55
- package/storage/ArchivesDisk.d.ts +67 -0
- package/storage/ArchivesDisk.ts +369 -0
- package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +1242 -0
- package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +513 -0
- package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +352 -0
- package/storage/BulkDatabase2/dist/BulkDatabaseReader.ts.cache +306 -0
- package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +435 -0
- package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +58 -0
- package/storage/BulkDatabase2/dist/blockCache.ts.cache +179 -0
- package/storage/BulkDatabase2/dist/mergeLock.ts.cache +149 -0
- package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +101 -0
- package/storage/BulkDatabase2/dist/streamLog.ts.cache +227 -0
- package/storage/BulkDatabase2/dist/syncClient.ts.cache +104 -0
- package/storage/IArchives.d.ts +127 -1
- package/storage/IArchives.ts +201 -1
- package/storage/backblaze.d.ts +29 -2
- package/storage/backblaze.ts +214 -78
- package/storage/dist/ArchivesDisk.ts.cache +373 -0
- package/storage/dist/FileFolderAPI.tsx.cache +508 -65
- package/storage/dist/IArchives.ts.cache +44 -0
- package/storage/dist/IndexedDBFileFolderAPI.ts.cache +8 -2
- package/storage/dist/JSONStorage.ts.cache +6 -2
- package/storage/dist/PendingManager.tsx.cache +1 -1
- package/storage/dist/TransactionStorage.ts.cache +231 -92
- package/storage/dist/backblaze.ts.cache +797 -0
- package/storage/dist/fileSystemPointer.ts.cache +36 -3
- package/storage/dist/remoteFileStorage.ts.cache +495 -0
- package/storage/remoteStorage/ArchivesRemote.d.ts +34 -18
- package/storage/remoteStorage/ArchivesRemote.ts +85 -65
- package/storage/remoteStorage/ArchivesUrl.d.ts +48 -0
- package/storage/remoteStorage/ArchivesUrl.ts +86 -0
- package/storage/remoteStorage/accessPage.tsx +111 -28
- package/storage/remoteStorage/blobStore.d.ts +128 -25
- package/storage/remoteStorage/blobStore.ts +757 -290
- package/storage/remoteStorage/cliArgs.d.ts +1 -0
- package/storage/remoteStorage/cliArgs.ts +9 -0
- package/storage/remoteStorage/createArchives.d.ts +85 -0
- package/storage/remoteStorage/createArchives.ts +612 -0
- package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +197 -0
- package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +85 -0
- package/storage/remoteStorage/dist/accessPage.tsx.cache +222 -0
- package/storage/remoteStorage/dist/blobStore.ts.cache +837 -0
- package/storage/remoteStorage/dist/cliArgs.ts.cache +16 -0
- package/storage/remoteStorage/dist/createArchives.ts.cache +610 -0
- package/storage/remoteStorage/dist/remoteConfig.ts.cache +206 -0
- package/storage/remoteStorage/dist/sourceWrapper.ts.cache +223 -0
- package/storage/remoteStorage/dist/storageController.ts.cache +500 -0
- package/storage/remoteStorage/dist/storageServer.ts.cache +106 -0
- package/storage/remoteStorage/dist/storageServerCli.ts.cache +39 -0
- package/storage/remoteStorage/dist/storageServerState.ts.cache +518 -0
- package/storage/remoteStorage/grantAccess.js +4 -0
- package/storage/remoteStorage/grantAccessCli.d.ts +1 -0
- package/storage/remoteStorage/grantAccessCli.ts +27 -0
- package/storage/remoteStorage/remoteConfig.d.ts +29 -0
- package/storage/remoteStorage/remoteConfig.ts +194 -0
- package/storage/remoteStorage/sourceWrapper.d.ts +41 -0
- package/storage/remoteStorage/sourceWrapper.ts +223 -0
- package/storage/remoteStorage/spec.md +31 -0
- package/storage/remoteStorage/storageController.d.ts +30 -27
- package/storage/remoteStorage/storageController.ts +297 -159
- package/storage/remoteStorage/storageServer.d.ts +6 -0
- package/storage/remoteStorage/storageServer.ts +88 -94
- package/storage/remoteStorage/storageServerCli.d.ts +1 -0
- package/storage/remoteStorage/storageServerCli.ts +38 -0
- package/storage/remoteStorage/storageServerState.d.ts +43 -0
- package/storage/remoteStorage/storageServerState.ts +540 -0
- package/testsite/server.ts +0 -2
- package/yarn.lock +11 -6
package/spec.txt
CHANGED
|
@@ -1,60 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
Searching
|
|
3
|
-
1) We want to create something that's database agnostic
|
|
4
|
-
- So I think we need like multiple phases for reading. And for applying it? We need to be able to know all of our reads ahead of time, which we should be able to roughly know. Or at least we'll have multiple stages. That should be fine. I guess we could do it so it actually understands our database. Like it reads, and then if it's not there, it returns. That might be fine too.
|
|
5
2
|
|
|
6
|
-
interface Database<Root> {
|
|
7
|
-
readData: <T>(fnc: (root: Root) => T) => T | undefined;
|
|
8
|
-
writeData: <T>(fnc: (root: Root) => T, newValue: T) => void;
|
|
9
|
-
}
|
|
10
|
-
I guess this works. We need multiple stages, so it's gonna have to read in the index, In parallel, and then once it has the full index, then it can do the next step in parallel, etc.
|
|
11
3
|
|
|
12
4
|
|
|
13
|
-
remaining
|
|
14
|
-
1) The code book creation. We kind of do want to recreate it sometimes. Maybe we can have something that does a full expensive recreation at certain early on points?
|
|
15
|
-
2) The searching there's a few different stages. Like, we want to search more IVF cells, but then even within the cells, we kind of want to download more of the final embeddings. because the codes aren't gonna to be perfect. So we have to do both at the same time?
|
|
16
5
|
|
|
17
|
-
parts
|
|
18
|
-
p8g8 format (q8), so we can store 2048 dimensions is a bit over 2KB
|
|
19
|
-
IVF cell size 32? PQ index reduces by 32X, and if IVF cells reduce by 32X... then 500K 2KB values (1GB) would still just be... 1MB IVF index, so... that's good.
|
|
20
|
-
0) In IVF, sample 32, then keep sampling more, up to 128 cells
|
|
21
|
-
1) Pq 64 index (byte values each, break into 64 groups, always... should work well, For face vectors, they're already really optimized. They have so few dimensions, and so we don't compress it by that much. But for text vectors, they're less compressed, so we compress it more. So it should work in all cases)
|
|
22
|
-
2) Retraining index on a subset
|
|
23
|
-
- Or... just don't. Freeze after a certain point, and... it might be fine? HMM... Although no, we could allow retraining. It should be cheap enough.
|
|
24
|
-
3) Split into transaction logs, so we can read the PQ index efficiently, but still update it
|
|
25
|
-
UGH... I mean basically you would just be replicating some of what we actually store on disk, but in our database, which is stupid, but I think we'll probably do it anyway. Basically, just a transaction log. We have to read everything every time, but it should help the speed a little bit...
|
|
26
|
-
- And I think it's fair because it's us saying, hey, we're going to read all this data every time, which the database can't know, so it can't optimize for that, but we can know, and then we can optimize for it
|
|
27
|
-
4) IVF the pq index to vastly decrease the initial access time
|
|
28
|
-
5.0) automatically split cells when they get big enough (2X their initial size)
|
|
29
|
-
5) occasionally regenerate the entire IVF.
|
|
30
|
-
5.1) Have something to store a subset of the data codes?
|
|
31
|
-
5.1) Have something that stores some of the data randomly so we can easily access a subset. We won't represent deletions in it. That's fine. This will allow us to rebalance the IVF fairly efficiently.
|
|
32
|
-
- And maybe even slowly move stuff over to the new IVF as we access stuff, we see if it's in the new IVF, and if not, we move it?
|
|
33
|
-
- 10% of the data? And... maybe just the pq index values?
|
|
34
|
-
- Should make retraining the IVF ten times faster and shouldn't affect the recall by too much, especially if we have more cells
|
|
35
|
-
- AND, Maybe we can actually sample less the more data we have. Ramping up to 10%.
|
|
36
|
-
unknown
|
|
37
|
-
exactly how we're going to update the IVF.
|
|
38
|
-
IVF pq index, so we can read part of it
|
|
39
|
-
mini transaction logs in each cell?
|
|
40
|
-
|
|
41
|
-
BUT... how do we update IVF
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
1) Converting once is faster, so we should investigate if we can have a pool of memory. If we can reuse it, then the allocations are free.
|
|
47
|
-
|
|
48
|
-
1) Fix embedding formats. It's really, really stupid. decodeToLength was just absolutely retarded.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
0) Make sure the AI stops trying to stop when a read didn't work because it doesn't fucking matter. I don't think there's a single place that's doing it where it actually matters.
|
|
56
|
-
- I mean, I guess there are a few places. Like when we're rebalancing, that's one place.
|
|
57
|
-
- I guess some of the returns are valid, but I guess we have to check all of them.
|
|
58
|
-
|
|
59
|
-
1) Make sure the AI gets rid of all those four each's. I mean, what the fuck?
|
|
60
|
-
1) Ensure the AI efficiently switches from the hard-coded stuff to the regular stuff and correctly regenerates the codebook.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
import { IArchives, ArchiveFileInfo, ArchivesConfig } from "./IArchives";
|
|
4
|
+
export declare class ArchivesDisk implements IArchives {
|
|
5
|
+
private folder;
|
|
6
|
+
constructor(folder: string);
|
|
7
|
+
private filesDir;
|
|
8
|
+
private uploadsDir;
|
|
9
|
+
private handles;
|
|
10
|
+
private largeUploads;
|
|
11
|
+
private nextLargeUploadId;
|
|
12
|
+
init: {
|
|
13
|
+
(): Promise<void>;
|
|
14
|
+
reset(): void;
|
|
15
|
+
set(newValue: Promise<void>): void;
|
|
16
|
+
};
|
|
17
|
+
getDebugName(): string;
|
|
18
|
+
getConfig(): Promise<ArchivesConfig>;
|
|
19
|
+
hasWriteAccess(): Promise<boolean>;
|
|
20
|
+
private filePath;
|
|
21
|
+
set(key: string, data: Buffer, config?: {
|
|
22
|
+
lastModified?: number;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
del(key: string): Promise<void>;
|
|
25
|
+
get(key: string, config?: {
|
|
26
|
+
range?: {
|
|
27
|
+
start: number;
|
|
28
|
+
end: number;
|
|
29
|
+
};
|
|
30
|
+
}): Promise<Buffer | undefined>;
|
|
31
|
+
get2(key: string, config?: {
|
|
32
|
+
range?: {
|
|
33
|
+
start: number;
|
|
34
|
+
end: number;
|
|
35
|
+
};
|
|
36
|
+
}): Promise<{
|
|
37
|
+
data: Buffer;
|
|
38
|
+
writeTime: number;
|
|
39
|
+
size: number;
|
|
40
|
+
} | undefined>;
|
|
41
|
+
getInfo(key: string): Promise<{
|
|
42
|
+
writeTime: number;
|
|
43
|
+
size: number;
|
|
44
|
+
} | undefined>;
|
|
45
|
+
find(prefix: string, config?: {
|
|
46
|
+
shallow?: boolean;
|
|
47
|
+
type: "files" | "folders";
|
|
48
|
+
}): Promise<string[]>;
|
|
49
|
+
findInfo(prefix: string, config?: {
|
|
50
|
+
shallow?: boolean;
|
|
51
|
+
type?: "files" | "folders";
|
|
52
|
+
}): Promise<ArchiveFileInfo[]>;
|
|
53
|
+
private collectFiles;
|
|
54
|
+
setLargeFile(config: {
|
|
55
|
+
path: string;
|
|
56
|
+
getNextData(): Promise<Buffer | undefined>;
|
|
57
|
+
}): Promise<void>;
|
|
58
|
+
startLargeUpload(): Promise<string>;
|
|
59
|
+
appendLargeUpload(id: string, data: Buffer): Promise<void>;
|
|
60
|
+
finishLargeUpload(id: string, key: string): Promise<void>;
|
|
61
|
+
cancelLargeUpload(id: string): Promise<void>;
|
|
62
|
+
getURL(path: string): Promise<string>;
|
|
63
|
+
}
|
|
64
|
+
export declare function applyFindInfoShape(files: ArchiveFileInfo[], prefix: string, config?: {
|
|
65
|
+
shallow?: boolean;
|
|
66
|
+
type?: "files" | "folders";
|
|
67
|
+
}): ArchiveFileInfo[];
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { lazy } from "socket-function/src/caching";
|
|
4
|
+
import { runInfinitePoll } from "socket-function/src/batching";
|
|
5
|
+
import { sort, binarySearchBasic } from "socket-function/src/misc";
|
|
6
|
+
import { IArchives, ArchiveFileInfo, ArchivesConfig, assertValidLastModified } from "./IArchives";
|
|
7
|
+
|
|
8
|
+
// The base file-system IArchives: storage is one-to-one with the file system, every key is exactly
|
|
9
|
+
// one real file under <folder>/files, so the file system itself is the index. File handles are
|
|
10
|
+
// cached and reused, and closed once idle (see FileHandleCache). All operations on a file run in
|
|
11
|
+
// serial, so they can't collide with each other or with handle closing. Used as the disk
|
|
12
|
+
// synchronization source of BlobStore (see remoteStorage/blobStore.ts).
|
|
13
|
+
|
|
14
|
+
const HANDLE_IDLE_TIMEOUT = 1000 * 60;
|
|
15
|
+
const HANDLE_SWEEP_INTERVAL = 1000 * 15;
|
|
16
|
+
|
|
17
|
+
type HandleEntry = {
|
|
18
|
+
filePath: string;
|
|
19
|
+
handle: fs.promises.FileHandle;
|
|
20
|
+
lastUse: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Caches open file handles, closing them once idle for HANDLE_IDLE_TIMEOUT. Instead of one
|
|
24
|
+
// setTimeout per handle, a list sorted by last use is swept periodically (entries are moved via
|
|
25
|
+
// binary search on access, and lastUse values only increase, so touched entries append at the
|
|
26
|
+
// end). Also serializes operations per file: each operation only starts once the previous one on
|
|
27
|
+
// the same file finished, and a handle is never closed while an operation is pending on it.
|
|
28
|
+
class FileHandleCache {
|
|
29
|
+
private entries = new Map<string, HandleEntry>();
|
|
30
|
+
// Sorted by lastUse ascending (least recently used first)
|
|
31
|
+
private lru: HandleEntry[] = [];
|
|
32
|
+
private pending = new Map<string, Promise<void>>();
|
|
33
|
+
|
|
34
|
+
constructor() {
|
|
35
|
+
runInfinitePoll(HANDLE_SWEEP_INTERVAL, () => this.sweep());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Runs fnc after every previously scheduled operation on filePath has finished
|
|
39
|
+
public run<T>(filePath: string, fnc: () => Promise<T>): Promise<T> {
|
|
40
|
+
let prev = this.pending.get(filePath) || Promise.resolve();
|
|
41
|
+
let result = prev.then(fnc);
|
|
42
|
+
// The pending chain must never reject, or one failed operation would poison all later ones
|
|
43
|
+
let last = result.then(() => { }, () => { });
|
|
44
|
+
this.pending.set(filePath, last);
|
|
45
|
+
void last.then(() => {
|
|
46
|
+
// If we're still the last pending operation on this file, clear ourselves
|
|
47
|
+
if (this.pending.get(filePath) === last) {
|
|
48
|
+
this.pending.delete(filePath);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Only call inside run() for the same filePath (so opens can't collide with closes)
|
|
55
|
+
public async getHandle(filePath: string, flags: number): Promise<fs.promises.FileHandle> {
|
|
56
|
+
let entry = this.entries.get(filePath);
|
|
57
|
+
if (entry) {
|
|
58
|
+
this.removeFromLRU(entry);
|
|
59
|
+
entry.lastUse = Date.now();
|
|
60
|
+
this.lru.push(entry);
|
|
61
|
+
return entry.handle;
|
|
62
|
+
}
|
|
63
|
+
let handle = await fs.promises.open(filePath, flags);
|
|
64
|
+
entry = { filePath, handle, lastUse: Date.now() };
|
|
65
|
+
this.entries.set(filePath, entry);
|
|
66
|
+
this.lru.push(entry);
|
|
67
|
+
return handle;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Only call inside run() for the same filePath
|
|
71
|
+
public async closeNow(filePath: string): Promise<void> {
|
|
72
|
+
let entry = this.entries.get(filePath);
|
|
73
|
+
if (!entry) return;
|
|
74
|
+
this.entries.delete(filePath);
|
|
75
|
+
this.removeFromLRU(entry);
|
|
76
|
+
await entry.handle.close();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private removeFromLRU(entry: HandleEntry) {
|
|
80
|
+
let index = binarySearchBasic(this.lru, x => x.lastUse, entry.lastUse);
|
|
81
|
+
if (index < 0) return;
|
|
82
|
+
// Multiple entries can share a lastUse, so scan for the exact one
|
|
83
|
+
while (index < this.lru.length && this.lru[index].lastUse === entry.lastUse) {
|
|
84
|
+
if (this.lru[index] === entry) {
|
|
85
|
+
this.lru.splice(index, 1);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
index++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private async sweep(): Promise<void> {
|
|
93
|
+
let cutoff = Date.now() - HANDLE_IDLE_TIMEOUT;
|
|
94
|
+
let index = 0;
|
|
95
|
+
while (index < this.lru.length && this.lru[index].lastUse <= cutoff) {
|
|
96
|
+
let entry = this.lru[index];
|
|
97
|
+
// Can't close a handle with a pending operation; it'll be swept after it finishes
|
|
98
|
+
if (this.pending.has(entry.filePath)) {
|
|
99
|
+
index++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
this.lru.splice(index, 1);
|
|
103
|
+
this.entries.delete(entry.filePath);
|
|
104
|
+
await entry.handle.close();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class ArchivesDisk implements IArchives {
|
|
110
|
+
constructor(private folder: string) { }
|
|
111
|
+
|
|
112
|
+
private filesDir = path.join(this.folder, "files");
|
|
113
|
+
private uploadsDir = path.join(this.folder, "uploads");
|
|
114
|
+
private handles = new FileHandleCache();
|
|
115
|
+
private largeUploads = new Map<string, { tmpPath: string }>();
|
|
116
|
+
private nextLargeUploadId = 1;
|
|
117
|
+
|
|
118
|
+
public init = lazy(async () => {
|
|
119
|
+
await fs.promises.mkdir(this.filesDir, { recursive: true });
|
|
120
|
+
await fs.promises.mkdir(this.uploadsDir, { recursive: true });
|
|
121
|
+
// Uploads don't survive restarts (the uploader streams into them), so old ones are garbage
|
|
122
|
+
for (let file of await fs.promises.readdir(this.uploadsDir)) {
|
|
123
|
+
await fs.promises.unlink(path.join(this.uploadsDir, file));
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
public getDebugName() {
|
|
128
|
+
return `disk/${this.folder}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public async getConfig(): Promise<ArchivesConfig> {
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public async hasWriteAccess(): Promise<boolean> {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private filePath(key: string): string {
|
|
140
|
+
let result = path.join(this.filesDir, key);
|
|
141
|
+
if (!result.startsWith(this.filesDir + path.sep)) {
|
|
142
|
+
throw new Error(`Invalid key ${JSON.stringify(key.slice(0, 200))}, it escapes the store folder`);
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public async set(key: string, data: Buffer, config?: { lastModified?: number }): Promise<void> {
|
|
148
|
+
await this.init();
|
|
149
|
+
let lastModified = config?.lastModified;
|
|
150
|
+
if (lastModified) {
|
|
151
|
+
assertValidLastModified(lastModified);
|
|
152
|
+
}
|
|
153
|
+
let filePath = this.filePath(key);
|
|
154
|
+
await this.handles.run(filePath, async () => {
|
|
155
|
+
if (lastModified) {
|
|
156
|
+
let existing = await statOrUndefined(filePath);
|
|
157
|
+
// An older write never overwrites a newer one (see IArchives.set)
|
|
158
|
+
if (existing && lastModified < existing.mtimeMs) return;
|
|
159
|
+
}
|
|
160
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
161
|
+
let handle = await this.handles.getHandle(filePath, fs.constants.O_RDWR | fs.constants.O_CREAT);
|
|
162
|
+
await handle.truncate(0);
|
|
163
|
+
await handle.write(data, 0, data.length, 0);
|
|
164
|
+
if (lastModified) {
|
|
165
|
+
await handle.utimes(new Date(lastModified), new Date(lastModified));
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
public async del(key: string): Promise<void> {
|
|
171
|
+
await this.init();
|
|
172
|
+
let filePath = this.filePath(key);
|
|
173
|
+
await this.handles.run(filePath, async () => {
|
|
174
|
+
await this.handles.closeNow(filePath);
|
|
175
|
+
try {
|
|
176
|
+
await fs.promises.unlink(filePath);
|
|
177
|
+
} catch (e: any) {
|
|
178
|
+
if (e.code !== "ENOENT") throw e;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public async get(key: string, config?: { range?: { start: number; end: number } }): Promise<Buffer | undefined> {
|
|
184
|
+
let result = await this.get2(key, config);
|
|
185
|
+
return result && result.data || undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public async get2(key: string, config?: { range?: { start: number; end: number } }): Promise<{ data: Buffer; writeTime: number; size: number } | undefined> {
|
|
189
|
+
await this.init();
|
|
190
|
+
let range = config?.range;
|
|
191
|
+
let filePath = this.filePath(key);
|
|
192
|
+
return await this.handles.run(filePath, async () => {
|
|
193
|
+
let handle: fs.promises.FileHandle;
|
|
194
|
+
try {
|
|
195
|
+
handle = await this.handles.getHandle(filePath, fs.constants.O_RDWR);
|
|
196
|
+
} catch (e: any) {
|
|
197
|
+
if (e.code === "ENOENT") return undefined;
|
|
198
|
+
throw e;
|
|
199
|
+
}
|
|
200
|
+
let stats = await handle.stat();
|
|
201
|
+
let size = stats.size;
|
|
202
|
+
let start = range && Math.min(range.start, size) || 0;
|
|
203
|
+
let end = range && Math.min(range.end, size) || size;
|
|
204
|
+
if (end <= start) return { data: Buffer.alloc(0), writeTime: stats.mtimeMs, size };
|
|
205
|
+
let buffer = Buffer.alloc(end - start);
|
|
206
|
+
let { bytesRead } = await handle.read(buffer, 0, buffer.length, start);
|
|
207
|
+
if (bytesRead !== buffer.length) {
|
|
208
|
+
throw new Error(`Expected ${buffer.length} bytes at ${filePath}:${start}, read ${bytesRead}`);
|
|
209
|
+
}
|
|
210
|
+
return { data: buffer, writeTime: stats.mtimeMs, size };
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
public async getInfo(key: string): Promise<{ writeTime: number; size: number } | undefined> {
|
|
215
|
+
await this.init();
|
|
216
|
+
let filePath = this.filePath(key);
|
|
217
|
+
return await this.handles.run(filePath, async () => {
|
|
218
|
+
let stats = await statOrUndefined(filePath);
|
|
219
|
+
if (!stats || !stats.isFile()) return undefined;
|
|
220
|
+
return { writeTime: stats.mtimeMs, size: stats.size };
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
public async find(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<string[]> {
|
|
225
|
+
return (await this.findInfo(prefix, config)).map(x => x.path);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
public async findInfo(prefix: string, config?: { shallow?: boolean; type?: "files" | "folders" }): Promise<ArchiveFileInfo[]> {
|
|
229
|
+
await this.init();
|
|
230
|
+
let infos = new Map<string, ArchiveFileInfo>();
|
|
231
|
+
await this.collectFiles("", prefix, infos);
|
|
232
|
+
let files = Array.from(infos.values());
|
|
233
|
+
files = applyFindInfoShape(files, prefix, config);
|
|
234
|
+
sort(files, x => x.path);
|
|
235
|
+
return files;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// relDir is "" or ends with "/". Only descends into directories that can still match the prefix.
|
|
239
|
+
private async collectFiles(relDir: string, prefix: string, infos: Map<string, ArchiveFileInfo>): Promise<void> {
|
|
240
|
+
let entries: fs.Dirent[];
|
|
241
|
+
try {
|
|
242
|
+
entries = await fs.promises.readdir(path.join(this.filesDir, relDir), { withFileTypes: true });
|
|
243
|
+
} catch (e: any) {
|
|
244
|
+
if (e.code === "ENOENT") return;
|
|
245
|
+
throw e;
|
|
246
|
+
}
|
|
247
|
+
for (let entry of entries) {
|
|
248
|
+
let relPath = relDir + entry.name;
|
|
249
|
+
if (entry.isDirectory()) {
|
|
250
|
+
let dirPath = relPath + "/";
|
|
251
|
+
if (dirPath.startsWith(prefix) || prefix.startsWith(dirPath)) {
|
|
252
|
+
await this.collectFiles(dirPath, prefix, infos);
|
|
253
|
+
}
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (!entry.isFile()) continue;
|
|
257
|
+
if (!relPath.startsWith(prefix)) continue;
|
|
258
|
+
let stats = await statOrUndefined(path.join(this.filesDir, relPath));
|
|
259
|
+
// Deleted while we were walking
|
|
260
|
+
if (!stats) continue;
|
|
261
|
+
infos.set(relPath, { path: relPath, createTime: stats.mtimeMs, size: stats.size });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
public async setLargeFile(config: { path: string; getNextData(): Promise<Buffer | undefined> }): Promise<void> {
|
|
266
|
+
let id = await this.startLargeUpload();
|
|
267
|
+
try {
|
|
268
|
+
while (true) {
|
|
269
|
+
let data = await config.getNextData();
|
|
270
|
+
if (!data) break;
|
|
271
|
+
await this.appendLargeUpload(id, data);
|
|
272
|
+
}
|
|
273
|
+
await this.finishLargeUpload(id, config.path);
|
|
274
|
+
} catch (e) {
|
|
275
|
+
await this.cancelLargeUpload(id);
|
|
276
|
+
throw e;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Large files stream into their own file under uploads/, then move into place on finish. No
|
|
281
|
+
// handle is opened until the first append actually happens.
|
|
282
|
+
public async startLargeUpload(): Promise<string> {
|
|
283
|
+
await this.init();
|
|
284
|
+
let id = `${Date.now()}_${this.nextLargeUploadId++}`;
|
|
285
|
+
this.largeUploads.set(id, { tmpPath: path.join(this.uploadsDir, `upload_${id}.tmp`) });
|
|
286
|
+
return id;
|
|
287
|
+
}
|
|
288
|
+
public async appendLargeUpload(id: string, data: Buffer): Promise<void> {
|
|
289
|
+
let upload = this.largeUploads.get(id);
|
|
290
|
+
if (!upload) throw new Error(`Unknown large upload ${id}`);
|
|
291
|
+
const tmpPath = upload.tmpPath;
|
|
292
|
+
await this.handles.run(tmpPath, async () => {
|
|
293
|
+
let handle = await this.handles.getHandle(tmpPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND);
|
|
294
|
+
await handle.write(data, 0, data.length);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
public async finishLargeUpload(id: string, key: string): Promise<void> {
|
|
298
|
+
let upload = this.largeUploads.get(id);
|
|
299
|
+
if (!upload) throw new Error(`Unknown large upload ${id}`);
|
|
300
|
+
const tmpPath = upload.tmpPath;
|
|
301
|
+
this.largeUploads.delete(id);
|
|
302
|
+
let filePath = this.filePath(key);
|
|
303
|
+
await this.handles.run(tmpPath, () => this.handles.closeNow(tmpPath));
|
|
304
|
+
await this.handles.run(filePath, async () => {
|
|
305
|
+
// Close any cached handle to the file we're replacing, so later reads reopen the new file
|
|
306
|
+
await this.handles.closeNow(filePath);
|
|
307
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
308
|
+
try {
|
|
309
|
+
await fs.promises.rename(tmpPath, filePath);
|
|
310
|
+
} catch (e: any) {
|
|
311
|
+
if (e.code !== "ENOENT") throw e;
|
|
312
|
+
// Nothing was ever appended, so the upload file was never created
|
|
313
|
+
await fs.promises.writeFile(filePath, Buffer.alloc(0));
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
public async cancelLargeUpload(id: string): Promise<void> {
|
|
318
|
+
let upload = this.largeUploads.get(id);
|
|
319
|
+
if (!upload) return;
|
|
320
|
+
const tmpPath = upload.tmpPath;
|
|
321
|
+
this.largeUploads.delete(id);
|
|
322
|
+
await this.handles.run(tmpPath, async () => {
|
|
323
|
+
await this.handles.closeNow(tmpPath);
|
|
324
|
+
try {
|
|
325
|
+
await fs.promises.unlink(tmpPath);
|
|
326
|
+
} catch (e: any) {
|
|
327
|
+
if (e.code !== "ENOENT") throw e;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
public async getURL(path: string): Promise<string> {
|
|
333
|
+
throw new Error(`getURL is not supported for disk archives (${this.getDebugName()}, path ${path})`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function statOrUndefined(filePath: string): Promise<fs.Stats | undefined> {
|
|
338
|
+
try {
|
|
339
|
+
return await fs.promises.stat(filePath);
|
|
340
|
+
} catch (e: any) {
|
|
341
|
+
if (e.code === "ENOENT") return undefined;
|
|
342
|
+
throw e;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// The folders/shallow post-processing shared by findInfo implementations that list flat files
|
|
347
|
+
// (used by ArchivesDisk on the raw disk walk, and by BlobStore on its index).
|
|
348
|
+
export function applyFindInfoShape(files: ArchiveFileInfo[], prefix: string, config?: { shallow?: boolean; type?: "files" | "folders" }): ArchiveFileInfo[] {
|
|
349
|
+
if (config?.type === "folders") {
|
|
350
|
+
let folders = new Map<string, ArchiveFileInfo>();
|
|
351
|
+
for (let file of files) {
|
|
352
|
+
let rest = file.path.slice(prefix.length);
|
|
353
|
+
let restParts = rest.split("/");
|
|
354
|
+
if (restParts.length < 2) continue;
|
|
355
|
+
let folder: string;
|
|
356
|
+
if (config.shallow) {
|
|
357
|
+
folder = prefix + restParts[0];
|
|
358
|
+
} else {
|
|
359
|
+
folder = file.path.split("/").slice(0, -1).join("/");
|
|
360
|
+
}
|
|
361
|
+
folders.set(folder, { path: folder, createTime: file.createTime, size: file.size });
|
|
362
|
+
}
|
|
363
|
+
return Array.from(folders.values());
|
|
364
|
+
}
|
|
365
|
+
if (config?.shallow) {
|
|
366
|
+
return files.filter(file => !file.path.slice(prefix.length).includes("/"));
|
|
367
|
+
}
|
|
368
|
+
return files;
|
|
369
|
+
}
|