@lacrous/omnicloud 0.2.26 → 0.2.28
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 +104 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +104 -2
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/server/index.js +105 -3
package/dist/index.d.cts
CHANGED
|
@@ -1106,6 +1106,8 @@ interface StorageUploadInput {
|
|
|
1106
1106
|
size?: number;
|
|
1107
1107
|
/** SHA-256 of the spooled bytes, computed while they were written. Required with `path`. */
|
|
1108
1108
|
sha256?: string;
|
|
1109
|
+
/** Message caption that identifies this upload, so a later search can find it. */
|
|
1110
|
+
caption?: string;
|
|
1109
1111
|
}
|
|
1110
1112
|
interface StorageHealth {
|
|
1111
1113
|
/** Reachable and usable. */
|
|
@@ -1137,6 +1139,12 @@ interface StorageProvider {
|
|
|
1137
1139
|
* the store and stop once `limit` objects are returned.
|
|
1138
1140
|
*/
|
|
1139
1141
|
listObjects(limit: number): Promise<StoredObject[]>;
|
|
1142
|
+
/**
|
|
1143
|
+
* Finds stored objects whose content hash and size match, read-only. Used to
|
|
1144
|
+
* recover an upload whose outcome was unknown. Providers that cannot search
|
|
1145
|
+
* omit it, and the caller then treats the outcome as unresolved.
|
|
1146
|
+
*/
|
|
1147
|
+
findByCaption?(caption: string, size: number): Promise<StoredObject[]>;
|
|
1140
1148
|
/** Validates that the backing store is reachable and usable. */
|
|
1141
1149
|
healthCheck(): Promise<StorageHealth>;
|
|
1142
1150
|
}
|
|
@@ -1186,6 +1194,8 @@ declare class StorageEngine {
|
|
|
1186
1194
|
exists(ref: StoredRef): Promise<boolean>;
|
|
1187
1195
|
/** Read-only listing of stored objects, for reconciliation. Never modifies anything. */
|
|
1188
1196
|
list(limit: number): Promise<StoredObject[]>;
|
|
1197
|
+
/** Read-only search for stored objects with this content hash and size. */
|
|
1198
|
+
findByCaption(caption: string, size: number): Promise<StoredObject[] | null>;
|
|
1189
1199
|
stat(ref: StoredRef): Promise<StoredObject | null>;
|
|
1190
1200
|
health(): Promise<StorageHealth>;
|
|
1191
1201
|
verifyIntegrity(data: Buffer, expectedSha256: string): Promise<boolean>;
|
|
@@ -1294,6 +1304,13 @@ declare class FileService {
|
|
|
1294
1304
|
* an operation whose object was already stored is committed without uploading.
|
|
1295
1305
|
*/
|
|
1296
1306
|
private uploadOnce;
|
|
1307
|
+
/**
|
|
1308
|
+
* Resolves an operation whose Telegram outcome is unknown. The channel is searched
|
|
1309
|
+
* for the object by its content hash and size. Exactly one match is adopted, zero
|
|
1310
|
+
* matches means nothing was stored so the upload is sent again, and anything
|
|
1311
|
+
* ambiguous stays UNKNOWN so that no duplicate is written.
|
|
1312
|
+
*/
|
|
1313
|
+
private resolveUnknown;
|
|
1297
1314
|
/**
|
|
1298
1315
|
* Follows an operation another request is driving, for a bounded time. Returns
|
|
1299
1316
|
* the finished file if the winner completes, or the stored object's commit if
|
|
@@ -1631,6 +1648,11 @@ declare class TelegramStorageProvider implements StorageProvider {
|
|
|
1631
1648
|
* service messages are skipped. Nothing is modified.
|
|
1632
1649
|
*/
|
|
1633
1650
|
listObjects(limit: number): Promise<StoredObject[]>;
|
|
1651
|
+
/**
|
|
1652
|
+
* Finds stored documents whose caption equals the given caption and whose size
|
|
1653
|
+
* matches. Used to recover an upload whose outcome was unknown.
|
|
1654
|
+
*/
|
|
1655
|
+
findByCaption(caption: string, size: number): Promise<StoredObject[]>;
|
|
1634
1656
|
stat(ref: StoredRef): Promise<StoredObject | null>;
|
|
1635
1657
|
/**
|
|
1636
1658
|
* Validates that the client is connected and the storage channel is
|
package/dist/index.d.ts
CHANGED
|
@@ -1106,6 +1106,8 @@ interface StorageUploadInput {
|
|
|
1106
1106
|
size?: number;
|
|
1107
1107
|
/** SHA-256 of the spooled bytes, computed while they were written. Required with `path`. */
|
|
1108
1108
|
sha256?: string;
|
|
1109
|
+
/** Message caption that identifies this upload, so a later search can find it. */
|
|
1110
|
+
caption?: string;
|
|
1109
1111
|
}
|
|
1110
1112
|
interface StorageHealth {
|
|
1111
1113
|
/** Reachable and usable. */
|
|
@@ -1137,6 +1139,12 @@ interface StorageProvider {
|
|
|
1137
1139
|
* the store and stop once `limit` objects are returned.
|
|
1138
1140
|
*/
|
|
1139
1141
|
listObjects(limit: number): Promise<StoredObject[]>;
|
|
1142
|
+
/**
|
|
1143
|
+
* Finds stored objects whose content hash and size match, read-only. Used to
|
|
1144
|
+
* recover an upload whose outcome was unknown. Providers that cannot search
|
|
1145
|
+
* omit it, and the caller then treats the outcome as unresolved.
|
|
1146
|
+
*/
|
|
1147
|
+
findByCaption?(caption: string, size: number): Promise<StoredObject[]>;
|
|
1140
1148
|
/** Validates that the backing store is reachable and usable. */
|
|
1141
1149
|
healthCheck(): Promise<StorageHealth>;
|
|
1142
1150
|
}
|
|
@@ -1186,6 +1194,8 @@ declare class StorageEngine {
|
|
|
1186
1194
|
exists(ref: StoredRef): Promise<boolean>;
|
|
1187
1195
|
/** Read-only listing of stored objects, for reconciliation. Never modifies anything. */
|
|
1188
1196
|
list(limit: number): Promise<StoredObject[]>;
|
|
1197
|
+
/** Read-only search for stored objects with this content hash and size. */
|
|
1198
|
+
findByCaption(caption: string, size: number): Promise<StoredObject[] | null>;
|
|
1189
1199
|
stat(ref: StoredRef): Promise<StoredObject | null>;
|
|
1190
1200
|
health(): Promise<StorageHealth>;
|
|
1191
1201
|
verifyIntegrity(data: Buffer, expectedSha256: string): Promise<boolean>;
|
|
@@ -1294,6 +1304,13 @@ declare class FileService {
|
|
|
1294
1304
|
* an operation whose object was already stored is committed without uploading.
|
|
1295
1305
|
*/
|
|
1296
1306
|
private uploadOnce;
|
|
1307
|
+
/**
|
|
1308
|
+
* Resolves an operation whose Telegram outcome is unknown. The channel is searched
|
|
1309
|
+
* for the object by its content hash and size. Exactly one match is adopted, zero
|
|
1310
|
+
* matches means nothing was stored so the upload is sent again, and anything
|
|
1311
|
+
* ambiguous stays UNKNOWN so that no duplicate is written.
|
|
1312
|
+
*/
|
|
1313
|
+
private resolveUnknown;
|
|
1297
1314
|
/**
|
|
1298
1315
|
* Follows an operation another request is driving, for a bounded time. Returns
|
|
1299
1316
|
* the finished file if the winner completes, or the stored object's commit if
|
|
@@ -1631,6 +1648,11 @@ declare class TelegramStorageProvider implements StorageProvider {
|
|
|
1631
1648
|
* service messages are skipped. Nothing is modified.
|
|
1632
1649
|
*/
|
|
1633
1650
|
listObjects(limit: number): Promise<StoredObject[]>;
|
|
1651
|
+
/**
|
|
1652
|
+
* Finds stored documents whose caption equals the given caption and whose size
|
|
1653
|
+
* matches. Used to recover an upload whose outcome was unknown.
|
|
1654
|
+
*/
|
|
1655
|
+
findByCaption(caption: string, size: number): Promise<StoredObject[]>;
|
|
1634
1656
|
stat(ref: StoredRef): Promise<StoredObject | null>;
|
|
1635
1657
|
/**
|
|
1636
1658
|
* 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(caption, size) {
|
|
1224
|
+
if (!this.provider.findByCaption) return null;
|
|
1225
|
+
try {
|
|
1226
|
+
return await this.provider.findByCaption(caption, 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,15 @@ 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 uploadCaption(sha256, operationId) {
|
|
1453
|
+
return `${sha256}:${operationId}`;
|
|
1454
|
+
}
|
|
1455
|
+
function contentSize(input) {
|
|
1456
|
+
return input.spooled?.size ?? input.data?.byteLength ?? 0;
|
|
1457
|
+
}
|
|
1440
1458
|
function connectionCausedFailure(error) {
|
|
1441
1459
|
if (error instanceof TelegramConnectionError) return true;
|
|
1442
1460
|
if (error instanceof UploadFailedError) {
|
|
@@ -1494,6 +1512,9 @@ var FileService = class {
|
|
|
1494
1512
|
if (op?.status === "UPLOADING" && op.telegramMessageId !== null) {
|
|
1495
1513
|
return this.commitStoredObject(userId, input, operations, op);
|
|
1496
1514
|
}
|
|
1515
|
+
if (op?.status === "UNKNOWN") {
|
|
1516
|
+
return this.resolveUnknown(userId, input, operations, op);
|
|
1517
|
+
}
|
|
1497
1518
|
if (!op) {
|
|
1498
1519
|
try {
|
|
1499
1520
|
op = await operations.create({ userId, operationId, requestFingerprint: fingerprint });
|
|
@@ -1512,6 +1533,7 @@ var FileService = class {
|
|
|
1512
1533
|
}
|
|
1513
1534
|
try {
|
|
1514
1535
|
const record = await this.performUpload(userId, input, {
|
|
1536
|
+
caption: uploadCaption(contentSha256(input), op.operationId),
|
|
1515
1537
|
onStored: async (stored) => {
|
|
1516
1538
|
await operations.update(op.id, { telegramMessageId: Number(stored.messageId) });
|
|
1517
1539
|
await operations.claim(op.id, "UPLOADING", "COMMITTING");
|
|
@@ -1523,11 +1545,54 @@ var FileService = class {
|
|
|
1523
1545
|
const status = connectionCausedFailure(error) ? "UNKNOWN" : "FAILED";
|
|
1524
1546
|
await operations.update(op.id, {
|
|
1525
1547
|
status,
|
|
1548
|
+
sha256: contentSha256(input),
|
|
1549
|
+
size: contentSize(input),
|
|
1526
1550
|
error: error instanceof Error ? error.message.slice(0, 200) : "upload failed"
|
|
1527
1551
|
});
|
|
1528
1552
|
throw error;
|
|
1529
1553
|
}
|
|
1530
1554
|
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Resolves an operation whose Telegram outcome is unknown. The channel is searched
|
|
1557
|
+
* for the object by its content hash and size. Exactly one match is adopted, zero
|
|
1558
|
+
* matches means nothing was stored so the upload is sent again, and anything
|
|
1559
|
+
* ambiguous stays UNKNOWN so that no duplicate is written.
|
|
1560
|
+
*/
|
|
1561
|
+
async resolveUnknown(userId, input, operations, op) {
|
|
1562
|
+
if (op.sha256 === null || op.size === null) {
|
|
1563
|
+
throw new ConflictError(
|
|
1564
|
+
"This upload's outcome is unknown and cannot be checked automatically. Ask an administrator to reconcile it."
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
const engine = await this.engineFor(userId);
|
|
1568
|
+
const matches = await engine.findByCaption(uploadCaption(op.sha256, op.operationId), op.size);
|
|
1569
|
+
if (matches === null) {
|
|
1570
|
+
throw new ConflictError(
|
|
1571
|
+
"This upload's outcome is unknown and the storage backend cannot search for it."
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
if (matches.length > 1) {
|
|
1575
|
+
throw new ConflictError(
|
|
1576
|
+
"This upload's outcome is unknown and more than one matching object exists. Nothing was written."
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
if (matches.length === 1) {
|
|
1580
|
+
const stored = matches[0];
|
|
1581
|
+
await operations.update(op.id, { telegramMessageId: Number(stored.messageId) });
|
|
1582
|
+
await operations.claim(op.id, "UNKNOWN", "COMMITTING");
|
|
1583
|
+
const record = await this.commitRecord(userId, input, {
|
|
1584
|
+
name: sanitizeFileName(input.name),
|
|
1585
|
+
mimeType: mimeFromFilename(sanitizeFileName(input.name)),
|
|
1586
|
+
size: op.size,
|
|
1587
|
+
sha256: op.sha256,
|
|
1588
|
+
telegramMessageId: Number(stored.messageId)
|
|
1589
|
+
});
|
|
1590
|
+
await operations.update(op.id, { status: "COMPLETED", fileId: record.id });
|
|
1591
|
+
return record;
|
|
1592
|
+
}
|
|
1593
|
+
await operations.claim(op.id, "UNKNOWN", "FAILED");
|
|
1594
|
+
return this.uploadOnce(userId, input, op.operationId, operations);
|
|
1595
|
+
}
|
|
1531
1596
|
/**
|
|
1532
1597
|
* Follows an operation another request is driving, for a bounded time. Returns
|
|
1533
1598
|
* the finished file if the winner completes, or the stored object's commit if
|
|
@@ -1605,8 +1670,9 @@ var FileService = class {
|
|
|
1605
1670
|
mimeType,
|
|
1606
1671
|
path: input.spooled.path,
|
|
1607
1672
|
size: input.spooled.size,
|
|
1608
|
-
sha256: input.spooled.sha256
|
|
1609
|
-
|
|
1673
|
+
sha256: input.spooled.sha256,
|
|
1674
|
+
caption: hooks.caption
|
|
1675
|
+
} : { name, mimeType, data: input.data, caption: hooks.caption },
|
|
1610
1676
|
control
|
|
1611
1677
|
);
|
|
1612
1678
|
await hooks.onStored?.(stored);
|
|
@@ -2974,6 +3040,7 @@ var TelegramStorageProvider = class {
|
|
|
2974
3040
|
file,
|
|
2975
3041
|
forceDocument: true,
|
|
2976
3042
|
workers: 1,
|
|
3043
|
+
caption: input.caption,
|
|
2977
3044
|
// Surface transport-level progress to the caller. GramJS reports a
|
|
2978
3045
|
// 0..1 fraction. `isCanceled` lets us abort an in-flight upload.
|
|
2979
3046
|
progressCallback: Object.assign(
|
|
@@ -3131,6 +3198,41 @@ var TelegramStorageProvider = class {
|
|
|
3131
3198
|
}
|
|
3132
3199
|
return objects;
|
|
3133
3200
|
}
|
|
3201
|
+
/**
|
|
3202
|
+
* Finds stored documents whose caption equals the given caption and whose size
|
|
3203
|
+
* matches. Used to recover an upload whose outcome was unknown.
|
|
3204
|
+
*/
|
|
3205
|
+
async findByCaption(caption, size) {
|
|
3206
|
+
const matches = [];
|
|
3207
|
+
try {
|
|
3208
|
+
for await (const message of this.client.iterMessages(this.peer, {
|
|
3209
|
+
search: caption,
|
|
3210
|
+
limit: 20
|
|
3211
|
+
})) {
|
|
3212
|
+
if (!(message instanceof Api2.Message)) continue;
|
|
3213
|
+
if (message.message !== caption) continue;
|
|
3214
|
+
const document = message.document;
|
|
3215
|
+
if (!(document instanceof Api2.Document)) continue;
|
|
3216
|
+
if (Number(document.size.toString()) !== size) continue;
|
|
3217
|
+
let fileName;
|
|
3218
|
+
for (const attr of document.attributes) {
|
|
3219
|
+
if (attr instanceof Api2.DocumentAttributeFilename) {
|
|
3220
|
+
fileName = attr.fileName;
|
|
3221
|
+
break;
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
matches.push({
|
|
3225
|
+
messageId: message.id.toString(),
|
|
3226
|
+
name: fileName ?? "file",
|
|
3227
|
+
size,
|
|
3228
|
+
mimeType: document.mimeType
|
|
3229
|
+
});
|
|
3230
|
+
}
|
|
3231
|
+
} catch (error) {
|
|
3232
|
+
throw mapTelegramError(error, "Telegram lookup failed");
|
|
3233
|
+
}
|
|
3234
|
+
return matches;
|
|
3235
|
+
}
|
|
3134
3236
|
async stat(ref) {
|
|
3135
3237
|
const message = await this.getMessage(ref.messageId);
|
|
3136
3238
|
if (!message?.media) return null;
|