diff --git a/src/domain/ports/bucket-repository.ts b/src/domain/ports/bucket-repository.ts new file mode 100644 index 0000000..6009d13 --- /dev/null +++ b/src/domain/ports/bucket-repository.ts @@ -0,0 +1,42 @@ +import type { Bucket } from '../entities/bucket'; + +/** + * Repository interface for Bucket entity persistence. + * + * Abstracts the bucket CRUD operations currently in `src/db/buckets.ts`. + */ +export interface IBucketRepository { + /** + * Create a new bucket with the given name. + * @param name - The unique bucket name (S3 naming convention). + * @returns The newly created bucket record. + */ + create(name: string): Promise; + + /** + * Find a bucket by its unique name. + * @param name - The bucket name to look up. + * @returns The matching bucket, or `null` when not found. + */ + findByName(name: string): Promise; + + /** + * List all buckets, ordered alphabetically by name. + * @returns An array of all bucket records. + */ + list(): Promise; + + /** + * Delete a bucket and cascade-delete all associated files and multipart data. + * @param name - The name of the bucket to delete. + * @returns `true` if the bucket was deleted, `false` if it did not exist. + */ + delete(name: string): Promise; + + /** + * Check whether a bucket with the given name exists. + * @param name - The bucket name to check. + * @returns `true` if the bucket exists, `false` otherwise. + */ + exists(name: string): Promise; +} diff --git a/src/domain/ports/file-part-repository.ts b/src/domain/ports/file-part-repository.ts new file mode 100644 index 0000000..16c9432 --- /dev/null +++ b/src/domain/ports/file-part-repository.ts @@ -0,0 +1,30 @@ +import type { FilePart, NewFilePart } from '../entities/file-part'; + +/** + * Repository interface for FilePart entity persistence. + * + * Abstracts the file-part operations currently in `src/db/file-parts.ts`. + * File parts represent the chunks of a large file stored across multiple + * Telegram messages for Telegram-safe storage. + */ +export interface IFilePartRepository { + /** + * Insert multiple file parts in a single operation. + * @param parts - An array of new file part records (auto-generated fields omitted). + */ + insert(parts: NewFilePart[]): Promise; + + /** + * List all file parts for a given file, ordered by part number. + * @param fileId - The UUID of the parent file record. + * @returns An array of file parts. + */ + listByFileId(fileId: string): Promise; + + /** + * Count the number of file parts associated with a file. + * @param fileId - The UUID of the parent file record. + * @returns The part count. + */ + countByFileId(fileId: string): Promise; +} diff --git a/src/domain/ports/file-repository.ts b/src/domain/ports/file-repository.ts new file mode 100644 index 0000000..5b648af --- /dev/null +++ b/src/domain/ports/file-repository.ts @@ -0,0 +1,108 @@ +import type { File, NewFile } from '../entities/file'; + +/** + * An S3-synced file record: a File entity guaranteed to have non-null + * bucketId and s3Key values. + */ +export interface S3FileRecord extends File { + /** S3 bucket UUID (non-null refinement) */ + bucketId: string; + /** S3 object key (non-null refinement) */ + s3Key: string; +} + +/** + * Repository interface for File entity persistence. + * + * Abstracts all file CRUD operations currently spread across + * `src/db/files.ts` and `src/db/files-ext.ts`. + */ +export interface IFileRepository { + /** + * Find a single file by its SHA-256 content hash. + * @param hash - The SHA-256 hash to search for. + * @returns The matching file, or `null` when not found. + */ + findByHash(hash: string): Promise; + + /** + * Find a single file by its public-facing short identifier. + * @param publicId - The public ID to look up. + * @returns The matching file, or `null` when not found. + */ + findByPublicId(publicId: string): Promise; + + /** + * Find a single file by its Telegram file unique ID (stable across bot tokens). + * @param telegramFileUniqueId - The Telegram unique file ID. + * @returns The matching file, or `null` when not found. + */ + findByUniqueId(telegramFileUniqueId: string): Promise; + + /** + * Find a single file by its S3 bucket and object key. + * @param bucketId - The bucket UUID. + * @param s3Key - The S3 object key. + * @returns The matching file, or `null` when not found. + */ + findByBucketAndKey(bucketId: string, s3Key: string): Promise; + + /** + * Create a new file record. + * @param file - The file data (auto-generated fields omitted). + * @returns The newly created file record with all fields populated. + */ + create(file: NewFile): Promise; + + /** + * List objects within a bucket, optionally filtered by prefix and delimiter. + * + * When `delimiter` is `"/"`, common prefixes (pseudo-directories) are + * returned separately and objects whose key continues past the delimiter + * are omitted from the `objects` array. + * + * @param bucketId - The bucket UUID to list from. + * @param prefix - Key prefix to filter by. + * @param delimiter - Delimiter character (e.g. `"/"`) or `null` for flat listing. + * @param maxKeys - Maximum number of object records to return. + * @param startAfter - Return only keys strictly greater than this value, or `null`. + * @returns A list of matching S3 file records and discovered common prefixes. + */ + listByPrefix( + bucketId: string, + prefix: string, + delimiter: string | null, + maxKeys: number, + startAfter: string | null, + ): Promise<{ objects: S3FileRecord[]; prefixes: string[] }>; + + /** + * Soft-delete a single file by bucket and key. + * @param bucketId - The bucket UUID. + * @param s3Key - The S3 object key. + * @returns `true` if a row was soft-deleted, `false` otherwise. + */ + softDelete(bucketId: string, s3Key: string): Promise; + + /** + * Soft-delete multiple files within a bucket in batch. + * @param bucketId - The bucket UUID. + * @param keys - Array of S3 object keys to delete. + * @returns The number of rows actually soft-deleted. + */ + softDeleteBatch(bucketId: string, keys: string[]): Promise; + + /** + * Count non-deleted objects in a bucket. + * @param bucketId - The bucket UUID. + * @returns The object count. + */ + countByBucket(bucketId: string): Promise; + + /** + * Find soft-deleted (orphaned) file records in a bucket. + * @param bucketId - The bucket UUID. + * @returns An array of orphaned file records. + */ + findOrphansByBucket(bucketId: string): Promise; +} diff --git a/src/domain/ports/multipart-repository.ts b/src/domain/ports/multipart-repository.ts new file mode 100644 index 0000000..ce35d4b --- /dev/null +++ b/src/domain/ports/multipart-repository.ts @@ -0,0 +1,70 @@ +import type { MultipartUpload, MultipartPart } from '../entities/multipart'; + +/** + * Repository interface for S3 multipart upload persistence. + * + * Abstracts the multipart upload operations currently in `src/db/multipart.ts`. + * Manages both multipart upload sessions and their individual parts. + */ +export interface IMultipartRepository { + /** + * Initiate a new multipart upload session. + * @param bucketId - The UUID of the target bucket. + * @param s3Key - The S3 object key being uploaded. + * @param initiatedBy - Identifier of the entity that initiated the upload. + * @returns The newly generated upload ID (nanoid). + */ + create(bucketId: string, s3Key: string, initiatedBy: string): Promise; + + /** + * Find an in-progress multipart upload by its upload ID. + * @param uploadId - The upload identifier. + * @returns The matching upload, or `null` if not found or not in progress. + */ + findById(uploadId: string): Promise; + + /** + * Mark a multipart upload as completed. + * @param uploadId - The upload identifier to complete. + */ + complete(uploadId: string): Promise; + + /** + * Mark a multipart upload as aborted. + * @param uploadId - The upload identifier to abort. + */ + abort(uploadId: string): Promise; + + /** + * Insert a single part record for a multipart upload. + * @param part - The part data (auto-generated fields omitted). + */ + insertPart(part: Omit): Promise; + + /** + * List all parts for a multipart upload, ordered by part number. + * @param uploadId - The upload identifier. + * @returns An array of multipart parts. + */ + listParts(uploadId: string): Promise; + + /** + * List in-progress multipart uploads within a bucket, with pagination. + * + * Results are ordered by S3 key and initiation timestamp. + * + * @param bucketId - The UUID of the bucket. + * @param maxUploads - Maximum number of uploads to return (clamped 1-1000). + * @param keyMarker - Return only uploads whose S3 key is strictly greater than this, or `null`. + * @returns A list of uploads and pagination metadata. + */ + listByBucket( + bucketId: string, + maxUploads: number, + keyMarker: string | null, + ): Promise<{ + uploads: MultipartUpload[]; + isTruncated: boolean; + nextKeyMarker: string | null; + }>; +} diff --git a/src/domain/ports/telegram-service.ts b/src/domain/ports/telegram-service.ts new file mode 100644 index 0000000..4bf9188 --- /dev/null +++ b/src/domain/ports/telegram-service.ts @@ -0,0 +1,68 @@ +/** + * Result of forwarding a file to Telegram storage. + */ +export interface ForwardResult { + /** The Telegram file_id for retrieving the file */ + telegramFileId: string; + /** The Telegram unique file_id (stable across bot tokens) */ + telegramFileUniqueId: string; + /** The message ID within the storage chat */ + storageMessageId: number; +} + +/** + * File information returned by Telegram's getFile API. + */ +export interface TelegramFileInfo { + /** File size in bytes */ + file_size: number; + /** MIME type of the file */ + mime_type: string; + /** Path on Telegram's file server for downloading */ + file_path: string; + /** Bot token that owns the retrieved file */ + bot_token: string; +} + +/** + * Abstraction over Telegram bot API operations. + * + * Defines the contract for forwarding files to Telegram storage, + * retrieving file metadata, and managing concurrent uploads. + */ +export interface ITelegramService { + /** + * Forward a file chunk to the configured Telegram storage chat. + * + * @param fileChunk - The file data (ReadStream, Buffer, or file path). + * @param fileName - The original file name. + * @param fileType - The file type classification (e.g. "photo", "document"). + * @returns The Telegram identifiers of the stored file. + */ + forwardToStorage( + fileChunk: unknown, + fileName: string, + fileType: string, + ): Promise; + + /** + * Retrieve file metadata from Telegram by file ID. + * + * Tries all configured bots; returns info from the first that owns the file. + * + * @param telegramFileId - The Telegram file_id to look up. + * @returns Metadata including size, MIME type, download path, and bot token. + */ + getFileInfo(telegramFileId: string): Promise; + + /** + * Enqueue a task for sequential upload execution. + * + * Ensures only one Telegram upload runs at a time to avoid + * rate limits and resource contention. + * + * @param task - An async function performing the upload. + * @returns The result of the task. + */ + enqueueUpload(task: () => Promise): Promise; +}