@lacrous/omnicloud 0.2.26 → 0.2.27
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/index.cjs +97 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +97 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/server/index.js +98 -1
package/dist/index.d.cts
CHANGED
|
@@ -1137,6 +1137,12 @@ interface StorageProvider {
|
|
|
1137
1137
|
* the store and stop once `limit` objects are returned.
|
|
1138
1138
|
*/
|
|
1139
1139
|
listObjects(limit: number): Promise<StoredObject[]>;
|
|
1140
|
+
/**
|
|
1141
|
+
* Finds stored objects whose content hash and size match, read-only. Used to
|
|
1142
|
+
* recover an upload whose outcome was unknown. Providers that cannot search
|
|
1143
|
+
* omit it, and the caller then treats the outcome as unresolved.
|
|
1144
|
+
*/
|
|
1145
|
+
findByCaption?(sha256: string, size: number): Promise<StoredObject[]>;
|
|
1140
1146
|
/** Validates that the backing store is reachable and usable. */
|
|
1141
1147
|
healthCheck(): Promise<StorageHealth>;
|
|
1142
1148
|
}
|
|
@@ -1186,6 +1192,8 @@ declare class StorageEngine {
|
|
|
1186
1192
|
exists(ref: StoredRef): Promise<boolean>;
|
|
1187
1193
|
/** Read-only listing of stored objects, for reconciliation. Never modifies anything. */
|
|
1188
1194
|
list(limit: number): Promise<StoredObject[]>;
|
|
1195
|
+
/** Read-only search for stored objects with this content hash and size. */
|
|
1196
|
+
findByCaption(sha256: string, size: number): Promise<StoredObject[] | null>;
|
|
1189
1197
|
stat(ref: StoredRef): Promise<StoredObject | null>;
|
|
1190
1198
|
health(): Promise<StorageHealth>;
|
|
1191
1199
|
verifyIntegrity(data: Buffer, expectedSha256: string): Promise<boolean>;
|
|
@@ -1294,6 +1302,13 @@ declare class FileService {
|
|
|
1294
1302
|
* an operation whose object was already stored is committed without uploading.
|
|
1295
1303
|
*/
|
|
1296
1304
|
private uploadOnce;
|
|
1305
|
+
/**
|
|
1306
|
+
* Resolves an operation whose Telegram outcome is unknown. The channel is searched
|
|
1307
|
+
* for the object by its content hash and size. Exactly one match is adopted, zero
|
|
1308
|
+
* matches means nothing was stored so the upload is sent again, and anything
|
|
1309
|
+
* ambiguous stays UNKNOWN so that no duplicate is written.
|
|
1310
|
+
*/
|
|
1311
|
+
private resolveUnknown;
|
|
1297
1312
|
/**
|
|
1298
1313
|
* Follows an operation another request is driving, for a bounded time. Returns
|
|
1299
1314
|
* the finished file if the winner completes, or the stored object's commit if
|
|
@@ -1631,6 +1646,11 @@ declare class TelegramStorageProvider implements StorageProvider {
|
|
|
1631
1646
|
* service messages are skipped. Nothing is modified.
|
|
1632
1647
|
*/
|
|
1633
1648
|
listObjects(limit: number): Promise<StoredObject[]>;
|
|
1649
|
+
/**
|
|
1650
|
+
* Finds stored documents whose caption equals the content hash and whose size
|
|
1651
|
+
* matches. Used to recover an upload whose outcome was unknown.
|
|
1652
|
+
*/
|
|
1653
|
+
findByCaption(sha256: string, size: number): Promise<StoredObject[]>;
|
|
1634
1654
|
stat(ref: StoredRef): Promise<StoredObject | null>;
|
|
1635
1655
|
/**
|
|
1636
1656
|
* Validates that the client is connected and the storage channel is
|
package/dist/index.d.ts
CHANGED
|
@@ -1137,6 +1137,12 @@ interface StorageProvider {
|
|
|
1137
1137
|
* the store and stop once `limit` objects are returned.
|
|
1138
1138
|
*/
|
|
1139
1139
|
listObjects(limit: number): Promise<StoredObject[]>;
|
|
1140
|
+
/**
|
|
1141
|
+
* Finds stored objects whose content hash and size match, read-only. Used to
|
|
1142
|
+
* recover an upload whose outcome was unknown. Providers that cannot search
|
|
1143
|
+
* omit it, and the caller then treats the outcome as unresolved.
|
|
1144
|
+
*/
|
|
1145
|
+
findByCaption?(sha256: string, size: number): Promise<StoredObject[]>;
|
|
1140
1146
|
/** Validates that the backing store is reachable and usable. */
|
|
1141
1147
|
healthCheck(): Promise<StorageHealth>;
|
|
1142
1148
|
}
|
|
@@ -1186,6 +1192,8 @@ declare class StorageEngine {
|
|
|
1186
1192
|
exists(ref: StoredRef): Promise<boolean>;
|
|
1187
1193
|
/** Read-only listing of stored objects, for reconciliation. Never modifies anything. */
|
|
1188
1194
|
list(limit: number): Promise<StoredObject[]>;
|
|
1195
|
+
/** Read-only search for stored objects with this content hash and size. */
|
|
1196
|
+
findByCaption(sha256: string, size: number): Promise<StoredObject[] | null>;
|
|
1189
1197
|
stat(ref: StoredRef): Promise<StoredObject | null>;
|
|
1190
1198
|
health(): Promise<StorageHealth>;
|
|
1191
1199
|
verifyIntegrity(data: Buffer, expectedSha256: string): Promise<boolean>;
|
|
@@ -1294,6 +1302,13 @@ declare class FileService {
|
|
|
1294
1302
|
* an operation whose object was already stored is committed without uploading.
|
|
1295
1303
|
*/
|
|
1296
1304
|
private uploadOnce;
|
|
1305
|
+
/**
|
|
1306
|
+
* Resolves an operation whose Telegram outcome is unknown. The channel is searched
|
|
1307
|
+
* for the object by its content hash and size. Exactly one match is adopted, zero
|
|
1308
|
+
* matches means nothing was stored so the upload is sent again, and anything
|
|
1309
|
+
* ambiguous stays UNKNOWN so that no duplicate is written.
|
|
1310
|
+
*/
|
|
1311
|
+
private resolveUnknown;
|
|
1297
1312
|
/**
|
|
1298
1313
|
* Follows an operation another request is driving, for a bounded time. Returns
|
|
1299
1314
|
* the finished file if the winner completes, or the stored object's commit if
|
|
@@ -1631,6 +1646,11 @@ declare class TelegramStorageProvider implements StorageProvider {
|
|
|
1631
1646
|
* service messages are skipped. Nothing is modified.
|
|
1632
1647
|
*/
|
|
1633
1648
|
listObjects(limit: number): Promise<StoredObject[]>;
|
|
1649
|
+
/**
|
|
1650
|
+
* Finds stored documents whose caption equals the content hash and whose size
|
|
1651
|
+
* matches. Used to recover an upload whose outcome was unknown.
|
|
1652
|
+
*/
|
|
1653
|
+
findByCaption(sha256: string, size: number): Promise<StoredObject[]>;
|
|
1634
1654
|
stat(ref: StoredRef): Promise<StoredObject | null>;
|
|
1635
1655
|
/**
|
|
1636
1656
|
* Validates that the client is connected and the storage channel is
|
package/dist/index.js
CHANGED
|
@@ -1219,6 +1219,15 @@ var StorageEngine = class {
|
|
|
1219
1219
|
throw mapProviderError("Storage listing failed", error);
|
|
1220
1220
|
}
|
|
1221
1221
|
}
|
|
1222
|
+
/** Read-only search for stored objects with this content hash and size. */
|
|
1223
|
+
async findByCaption(sha256, size) {
|
|
1224
|
+
if (!this.provider.findByCaption) return null;
|
|
1225
|
+
try {
|
|
1226
|
+
return await this.provider.findByCaption(sha256, size);
|
|
1227
|
+
} catch (error) {
|
|
1228
|
+
throw mapProviderError("Storage lookup failed", error);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1222
1231
|
async stat(ref) {
|
|
1223
1232
|
try {
|
|
1224
1233
|
return await this.provider.stat(ref);
|
|
@@ -1437,6 +1446,12 @@ function uploadRequestFingerprint(input) {
|
|
|
1437
1446
|
}
|
|
1438
1447
|
|
|
1439
1448
|
// ../core/src/services/file-service.ts
|
|
1449
|
+
function contentSha256(input) {
|
|
1450
|
+
return input.spooled?.sha256 ?? sha256Hex(input.data ?? Buffer.alloc(0));
|
|
1451
|
+
}
|
|
1452
|
+
function contentSize(input) {
|
|
1453
|
+
return input.spooled?.size ?? input.data?.byteLength ?? 0;
|
|
1454
|
+
}
|
|
1440
1455
|
function connectionCausedFailure(error) {
|
|
1441
1456
|
if (error instanceof TelegramConnectionError) return true;
|
|
1442
1457
|
if (error instanceof UploadFailedError) {
|
|
@@ -1494,6 +1509,9 @@ var FileService = class {
|
|
|
1494
1509
|
if (op?.status === "UPLOADING" && op.telegramMessageId !== null) {
|
|
1495
1510
|
return this.commitStoredObject(userId, input, operations, op);
|
|
1496
1511
|
}
|
|
1512
|
+
if (op?.status === "UNKNOWN") {
|
|
1513
|
+
return this.resolveUnknown(userId, input, operations, op);
|
|
1514
|
+
}
|
|
1497
1515
|
if (!op) {
|
|
1498
1516
|
try {
|
|
1499
1517
|
op = await operations.create({ userId, operationId, requestFingerprint: fingerprint });
|
|
@@ -1523,11 +1541,54 @@ var FileService = class {
|
|
|
1523
1541
|
const status = connectionCausedFailure(error) ? "UNKNOWN" : "FAILED";
|
|
1524
1542
|
await operations.update(op.id, {
|
|
1525
1543
|
status,
|
|
1544
|
+
sha256: contentSha256(input),
|
|
1545
|
+
size: contentSize(input),
|
|
1526
1546
|
error: error instanceof Error ? error.message.slice(0, 200) : "upload failed"
|
|
1527
1547
|
});
|
|
1528
1548
|
throw error;
|
|
1529
1549
|
}
|
|
1530
1550
|
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Resolves an operation whose Telegram outcome is unknown. The channel is searched
|
|
1553
|
+
* for the object by its content hash and size. Exactly one match is adopted, zero
|
|
1554
|
+
* matches means nothing was stored so the upload is sent again, and anything
|
|
1555
|
+
* ambiguous stays UNKNOWN so that no duplicate is written.
|
|
1556
|
+
*/
|
|
1557
|
+
async resolveUnknown(userId, input, operations, op) {
|
|
1558
|
+
if (op.sha256 === null || op.size === null) {
|
|
1559
|
+
throw new ConflictError(
|
|
1560
|
+
"This upload's outcome is unknown and cannot be checked automatically. Ask an administrator to reconcile it."
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
const engine = await this.engineFor(userId);
|
|
1564
|
+
const matches = await engine.findByCaption(op.sha256, op.size);
|
|
1565
|
+
if (matches === null) {
|
|
1566
|
+
throw new ConflictError(
|
|
1567
|
+
"This upload's outcome is unknown and the storage backend cannot search for it."
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
if (matches.length > 1) {
|
|
1571
|
+
throw new ConflictError(
|
|
1572
|
+
"This upload's outcome is unknown and more than one matching object exists. Nothing was written."
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
if (matches.length === 1) {
|
|
1576
|
+
const stored = matches[0];
|
|
1577
|
+
await operations.update(op.id, { telegramMessageId: Number(stored.messageId) });
|
|
1578
|
+
await operations.claim(op.id, "UNKNOWN", "COMMITTING");
|
|
1579
|
+
const record = await this.commitRecord(userId, input, {
|
|
1580
|
+
name: sanitizeFileName(input.name),
|
|
1581
|
+
mimeType: mimeFromFilename(sanitizeFileName(input.name)),
|
|
1582
|
+
size: op.size,
|
|
1583
|
+
sha256: op.sha256,
|
|
1584
|
+
telegramMessageId: Number(stored.messageId)
|
|
1585
|
+
});
|
|
1586
|
+
await operations.update(op.id, { status: "COMPLETED", fileId: record.id });
|
|
1587
|
+
return record;
|
|
1588
|
+
}
|
|
1589
|
+
await operations.claim(op.id, "UNKNOWN", "FAILED");
|
|
1590
|
+
return this.uploadOnce(userId, input, op.operationId, operations);
|
|
1591
|
+
}
|
|
1531
1592
|
/**
|
|
1532
1593
|
* Follows an operation another request is driving, for a bounded time. Returns
|
|
1533
1594
|
* the finished file if the winner completes, or the stored object's commit if
|
|
@@ -2974,6 +3035,7 @@ var TelegramStorageProvider = class {
|
|
|
2974
3035
|
file,
|
|
2975
3036
|
forceDocument: true,
|
|
2976
3037
|
workers: 1,
|
|
3038
|
+
caption: input.sha256,
|
|
2977
3039
|
// Surface transport-level progress to the caller. GramJS reports a
|
|
2978
3040
|
// 0..1 fraction. `isCanceled` lets us abort an in-flight upload.
|
|
2979
3041
|
progressCallback: Object.assign(
|
|
@@ -3131,6 +3193,41 @@ var TelegramStorageProvider = class {
|
|
|
3131
3193
|
}
|
|
3132
3194
|
return objects;
|
|
3133
3195
|
}
|
|
3196
|
+
/**
|
|
3197
|
+
* Finds stored documents whose caption equals the content hash and whose size
|
|
3198
|
+
* matches. Used to recover an upload whose outcome was unknown.
|
|
3199
|
+
*/
|
|
3200
|
+
async findByCaption(sha256, size) {
|
|
3201
|
+
const matches = [];
|
|
3202
|
+
try {
|
|
3203
|
+
for await (const message of this.client.iterMessages(this.peer, {
|
|
3204
|
+
search: sha256,
|
|
3205
|
+
limit: 20
|
|
3206
|
+
})) {
|
|
3207
|
+
if (!(message instanceof Api2.Message)) continue;
|
|
3208
|
+
if (message.message !== sha256) continue;
|
|
3209
|
+
const document = message.document;
|
|
3210
|
+
if (!(document instanceof Api2.Document)) continue;
|
|
3211
|
+
if (Number(document.size.toString()) !== size) continue;
|
|
3212
|
+
let fileName;
|
|
3213
|
+
for (const attr of document.attributes) {
|
|
3214
|
+
if (attr instanceof Api2.DocumentAttributeFilename) {
|
|
3215
|
+
fileName = attr.fileName;
|
|
3216
|
+
break;
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
matches.push({
|
|
3220
|
+
messageId: message.id.toString(),
|
|
3221
|
+
name: fileName ?? "file",
|
|
3222
|
+
size,
|
|
3223
|
+
mimeType: document.mimeType
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
} catch (error) {
|
|
3227
|
+
throw mapTelegramError(error, "Telegram lookup failed");
|
|
3228
|
+
}
|
|
3229
|
+
return matches;
|
|
3230
|
+
}
|
|
3134
3231
|
async stat(ref) {
|
|
3135
3232
|
const message = await this.getMessage(ref.messageId);
|
|
3136
3233
|
if (!message?.media) return null;
|