@nebutra/uploads 0.1.0 → 0.1.2

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.
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Upload provider factory with auto-detection
3
+ */
4
+ import type { ProviderConfig, UploadProvider, UploadProviderType } from "./types";
5
+ /**
6
+ * Create an upload provider with auto-detection
7
+ */
8
+ export declare function createUploadProvider(config?: ProviderConfig): UploadProvider;
9
+ /**
10
+ * Get or create singleton instance
11
+ */
12
+ export declare function getUploadProvider(config?: ProviderConfig): Promise<UploadProvider>;
13
+ /**
14
+ * Reset singleton instance (for testing)
15
+ */
16
+ export declare function resetUploadProvider(): void;
17
+ /**
18
+ * Determine which provider is active
19
+ */
20
+ export declare function getActiveProviderType(): UploadProviderType;
21
+ //# sourceMappingURL=factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAIlF;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,cAAc,CAM5E;AAkFD;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAOxF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAO1C;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,kBAAkB,CAyB1D"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Upload provider factory with auto-detection
3
+ */
4
+ import { logger } from "@nebutra/logger";
5
+ import { createVercelBlobProvider, VercelBlobProvider } from "./providers/blob";
6
+ import { createLocalProvider, LocalUploadProvider } from "./providers/local";
7
+ import { createS3Provider, S3UploadProvider } from "./providers/s3";
8
+ let instance = null;
9
+ /**
10
+ * Create an upload provider with auto-detection
11
+ */
12
+ export function createUploadProvider(config) {
13
+ if (config?.type) {
14
+ return createUploadProviderByType(config.type, config);
15
+ }
16
+ return autoDetectProvider(config);
17
+ }
18
+ /**
19
+ * Create provider by explicit type
20
+ */
21
+ function createUploadProviderByType(type, config) {
22
+ if (type === "s3") {
23
+ if (!config?.s3) {
24
+ return createS3Provider();
25
+ }
26
+ return new S3UploadProvider(config.s3);
27
+ }
28
+ if (type === "blob") {
29
+ return createVercelBlobProvider();
30
+ }
31
+ if (type === "local") {
32
+ if (!config?.local) {
33
+ return createLocalProvider();
34
+ }
35
+ return new LocalUploadProvider(config.local);
36
+ }
37
+ throw new Error(`Unknown upload provider type: ${type}`);
38
+ }
39
+ /**
40
+ * Auto-detect provider based on environment
41
+ */
42
+ function autoDetectProvider(config) {
43
+ // Priority 1: Check explicit UPLOAD_PROVIDER
44
+ const explicitProvider = process.env.UPLOAD_PROVIDER;
45
+ if (explicitProvider) {
46
+ logger.info("Using explicit upload provider", { provider: explicitProvider });
47
+ return createUploadProviderByType(explicitProvider, config);
48
+ }
49
+ // Priority 2: Check for Vercel Blob token (zero-config on Vercel)
50
+ if (process.env.BLOB_READ_WRITE_TOKEN) {
51
+ logger.info("Auto-detected Vercel Blob token, using Blob provider");
52
+ return createVercelBlobProvider();
53
+ }
54
+ // Priority 3: Check for R2 credentials
55
+ if (process.env.R2_ACCESS_KEY_ID || process.env.R2_SECRET_ACCESS_KEY) {
56
+ logger.info("Auto-detected R2 credentials, using S3 provider");
57
+ return new S3UploadProvider({
58
+ accessKeyId: process.env.R2_ACCESS_KEY_ID || "",
59
+ secretAccessKey: process.env.R2_SECRET_ACCESS_KEY || "",
60
+ region: "auto",
61
+ ...(process.env.R2_ENDPOINT !== undefined ? { endpoint: process.env.R2_ENDPOINT } : {}),
62
+ ...(process.env.R2_PUBLIC_URL !== undefined ? { publicUrl: process.env.R2_PUBLIC_URL } : {}),
63
+ });
64
+ }
65
+ // Priority 3: Check for AWS S3 credentials
66
+ if (process.env.AWS_ACCESS_KEY_ID || process.env.AWS_SECRET_ACCESS_KEY) {
67
+ logger.info("Auto-detected AWS credentials, using S3 provider");
68
+ return createS3Provider();
69
+ }
70
+ // Priority 4: Check for custom S3 endpoint (Minio, etc.)
71
+ if (process.env.S3_ENDPOINT) {
72
+ logger.info("Using custom S3 endpoint", { endpoint: process.env.S3_ENDPOINT });
73
+ return new S3UploadProvider({
74
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID || process.env.S3_ACCESS_KEY_ID || "",
75
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || process.env.S3_SECRET_ACCESS_KEY || "",
76
+ region: process.env.AWS_REGION || "us-east-1",
77
+ ...(process.env.S3_ENDPOINT !== undefined ? { endpoint: process.env.S3_ENDPOINT } : {}),
78
+ ...(process.env.S3_PUBLIC_URL !== undefined ? { publicUrl: process.env.S3_PUBLIC_URL } : {}),
79
+ });
80
+ }
81
+ // Fallback: Local filesystem
82
+ logger.info("No cloud credentials found, using local filesystem provider");
83
+ return createLocalProvider();
84
+ }
85
+ /**
86
+ * Get or create singleton instance
87
+ */
88
+ export async function getUploadProvider(config) {
89
+ if (instance) {
90
+ return instance;
91
+ }
92
+ instance = createUploadProvider(config);
93
+ return instance;
94
+ }
95
+ /**
96
+ * Reset singleton instance (for testing)
97
+ */
98
+ export function resetUploadProvider() {
99
+ if (instance) {
100
+ instance.close().catch((error) => {
101
+ logger.warn("Failed to close upload provider", { error });
102
+ });
103
+ }
104
+ instance = null;
105
+ }
106
+ /**
107
+ * Determine which provider is active
108
+ */
109
+ export function getActiveProviderType() {
110
+ if (instance instanceof S3UploadProvider) {
111
+ return "s3";
112
+ }
113
+ if (instance instanceof VercelBlobProvider) {
114
+ return "blob";
115
+ }
116
+ if (instance instanceof LocalUploadProvider) {
117
+ return "local";
118
+ }
119
+ if (process.env.UPLOAD_PROVIDER === "blob" || process.env.BLOB_READ_WRITE_TOKEN) {
120
+ return "blob";
121
+ }
122
+ if (process.env.UPLOAD_PROVIDER === "s3" ||
123
+ process.env.R2_ACCESS_KEY_ID ||
124
+ process.env.AWS_ACCESS_KEY_ID ||
125
+ process.env.S3_ENDPOINT) {
126
+ return "s3";
127
+ }
128
+ return "local";
129
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @nebutra/uploads — Large file upload service
3
+ * Handles presigned, multipart, and Tus protocol uploads
4
+ */
5
+ export { createUploadProvider, getActiveProviderType, getUploadProvider, resetUploadProvider, } from "./factory";
6
+ export { createVercelBlobProvider, VercelBlobProvider } from "./providers/blob";
7
+ export { createLocalProvider, LocalUploadProvider } from "./providers/local";
8
+ export { createS3Provider, S3UploadProvider } from "./providers/s3";
9
+ export type { ACLType, CompletePart, LocalProviderConfig, MultipartUpload, PresignedUpload, ProviderConfig, S3ProviderConfig, TusUploadConfig, UploadComplete, UploadMethod, UploadPart, UploadProvider, UploadProviderType, UploadTarget, ValidationRules, } from "./types";
10
+ export { createDocumentValidationRules, createImageValidationRules, logValidationError, type ValidationError, validateImageDimensions, validateUpload, } from "./validation";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEpE,YAAY,EACV,OAAO,EACP,YAAY,EACZ,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,YAAY,EACZ,UAAU,EACV,cAAc,EACd,kBAAkB,EAClB,YAAY,EACZ,eAAe,GAChB,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,6BAA6B,EAC7B,0BAA0B,EAC1B,kBAAkB,EAClB,KAAK,eAAe,EACpB,uBAAuB,EACvB,cAAc,GACf,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @nebutra/uploads — Large file upload service
3
+ * Handles presigned, multipart, and Tus protocol uploads
4
+ */
5
+ // Factory
6
+ export { createUploadProvider, getActiveProviderType, getUploadProvider, resetUploadProvider, } from "./factory";
7
+ // Providers
8
+ export { createVercelBlobProvider, VercelBlobProvider } from "./providers/blob";
9
+ export { createLocalProvider, LocalUploadProvider } from "./providers/local";
10
+ export { createS3Provider, S3UploadProvider } from "./providers/s3";
11
+ // Validation
12
+ export { createDocumentValidationRules, createImageValidationRules, logValidationError, validateImageDimensions, validateUpload, } from "./validation";
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Vercel Blob upload provider — zero-config file uploads on Vercel.
3
+ *
4
+ * Uses @vercel/blob for server-side uploads with public URLs.
5
+ * Requires BLOB_READ_WRITE_TOKEN env var (auto-provisioned on Vercel).
6
+ *
7
+ * Note: Vercel Blob doesn't support true multipart uploads or Tus protocol.
8
+ * For files > 500MB, use the S3 provider instead.
9
+ */
10
+ import type { CompletePart, MultipartUpload, PresignedUpload, TusUploadConfig, UploadComplete, UploadProvider, UploadTarget } from "../types";
11
+ export interface VercelBlobConfig {
12
+ token?: string;
13
+ }
14
+ export declare class VercelBlobProvider implements UploadProvider {
15
+ private token;
16
+ constructor(config?: VercelBlobConfig);
17
+ private buildPathname;
18
+ createPresignedUpload(target: UploadTarget): Promise<PresignedUpload>;
19
+ createMultipartUpload(target: UploadTarget, _partCount: number): Promise<MultipartUpload>;
20
+ completeMultipartUpload(_uploadId: string, key: string, _parts: CompletePart[]): Promise<UploadComplete>;
21
+ abortMultipartUpload(_uploadId: string, key: string): Promise<void>;
22
+ getTusEndpoint(_target: UploadTarget): Promise<TusUploadConfig>;
23
+ deleteFile(_bucket: string, key: string): Promise<void>;
24
+ getDownloadUrl(_bucket: string, key: string, _expiresInSec?: number): Promise<string>;
25
+ close(): Promise<void>;
26
+ }
27
+ export declare function createVercelBlobProvider(config?: VercelBlobConfig): VercelBlobProvider;
28
+ //# sourceMappingURL=blob.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blob.d.ts","sourceRoot":"","sources":["../../src/providers/blob.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,eAAe,EACf,eAAe,EACf,eAAe,EACf,cAAc,EACd,cAAc,EACd,YAAY,EACb,MAAM,UAAU,CAAC;AAElB,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,kBAAmB,YAAW,cAAc;IACvD,OAAO,CAAC,KAAK,CAAS;gBAEV,MAAM,GAAE,gBAAqB;IAIzC,OAAO,CAAC,aAAa;IAKf,qBAAqB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC;IAyBrE,qBAAqB,CAAC,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAazF,uBAAuB,CAC3B,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,YAAY,EAAE,GACrB,OAAO,CAAC,cAAc,CAAC;IAWpB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKnE,cAAc,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC;IAM/D,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMvD,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAQrF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B;AAED,wBAAgB,wBAAwB,CAAC,MAAM,CAAC,EAAE,gBAAgB,GAAG,kBAAkB,CAEtF"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Vercel Blob upload provider — zero-config file uploads on Vercel.
3
+ *
4
+ * Uses @vercel/blob for server-side uploads with public URLs.
5
+ * Requires BLOB_READ_WRITE_TOKEN env var (auto-provisioned on Vercel).
6
+ *
7
+ * Note: Vercel Blob doesn't support true multipart uploads or Tus protocol.
8
+ * For files > 500MB, use the S3 provider instead.
9
+ */
10
+ export class VercelBlobProvider {
11
+ token;
12
+ constructor(config = {}) {
13
+ this.token = config.token ?? process.env.BLOB_READ_WRITE_TOKEN ?? "";
14
+ }
15
+ buildPathname(target) {
16
+ const prefix = target.tenantId ? `${target.tenantId}/` : "";
17
+ return `${prefix}${target.key}`;
18
+ }
19
+ async createPresignedUpload(target) {
20
+ // Vercel Blob uses server-side put() — we generate a URL that the client
21
+ // can PUT to via the handleUpload client helper, or we return the blob URL
22
+ // after server-side upload.
23
+ const blob = await import("@vercel/blob");
24
+ const pathname = this.buildPathname(target);
25
+ const result = await blob.put(pathname, "", {
26
+ access: "public",
27
+ contentType: target.contentType,
28
+ token: this.token,
29
+ addRandomSuffix: true,
30
+ });
31
+ return {
32
+ url: result.url,
33
+ method: "PUT",
34
+ headers: {
35
+ "Content-Type": target.contentType,
36
+ "x-vercel-blob-token": this.token,
37
+ },
38
+ expiresAt: new Date(Date.now() + 3600 * 1000), // 1 hour
39
+ };
40
+ }
41
+ async createMultipartUpload(target, _partCount) {
42
+ // Vercel Blob handles large files internally — no client-side multipart needed
43
+ // We simulate the interface for compatibility
44
+ const presigned = await this.createPresignedUpload(target);
45
+ return {
46
+ uploadId: `blob_${Date.now()}`,
47
+ key: this.buildPathname(target),
48
+ bucket: target.bucket,
49
+ parts: [{ partNumber: 1, presignedUrl: presigned.url }],
50
+ completeUrl: presigned.url,
51
+ };
52
+ }
53
+ async completeMultipartUpload(_uploadId, key, _parts) {
54
+ // Vercel Blob uploads are atomic — no completion step needed
55
+ return {
56
+ key,
57
+ bucket: "vercel-blob",
58
+ url: `https://blob.vercel-storage.com/${key}`,
59
+ size: 0,
60
+ contentType: "application/octet-stream",
61
+ };
62
+ }
63
+ async abortMultipartUpload(_uploadId, key) {
64
+ // Clean up the blob if partially uploaded
65
+ await this.deleteFile("vercel-blob", key);
66
+ }
67
+ async getTusEndpoint(_target) {
68
+ throw new Error("Vercel Blob does not support Tus protocol. Use S3 provider for resumable uploads.");
69
+ }
70
+ async deleteFile(_bucket, key) {
71
+ const blob = await import("@vercel/blob");
72
+ const url = key.startsWith("http") ? key : `https://blob.vercel-storage.com/${key}`;
73
+ await blob.del(url, { token: this.token });
74
+ }
75
+ async getDownloadUrl(_bucket, key, _expiresInSec) {
76
+ // Vercel Blob URLs are public by default — no signed URL needed
77
+ const blob = await import("@vercel/blob");
78
+ const url = key.startsWith("http") ? key : `https://blob.vercel-storage.com/${key}`;
79
+ const meta = await blob.head(url, { token: this.token });
80
+ return meta.url;
81
+ }
82
+ async close() {
83
+ // No-op — Vercel Blob has no persistent connections
84
+ }
85
+ }
86
+ export function createVercelBlobProvider(config) {
87
+ return new VercelBlobProvider(config);
88
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Local filesystem upload provider
3
+ * For development and testing
4
+ */
5
+ import type { CompletePart, LocalProviderConfig, MultipartUpload, PresignedUpload, TusUploadConfig, UploadComplete, UploadProvider, UploadTarget } from "../types";
6
+ export declare class LocalUploadProvider implements UploadProvider {
7
+ private config;
8
+ private pendingUploads;
9
+ constructor(config: LocalProviderConfig);
10
+ /**
11
+ * Build object key with tenant prefix and UUID
12
+ */
13
+ private buildKey;
14
+ /**
15
+ * Generate presigned URL for simple uploads
16
+ */
17
+ createPresignedUpload(target: UploadTarget): Promise<PresignedUpload>;
18
+ /**
19
+ * Initiate multipart upload
20
+ */
21
+ createMultipartUpload(target: UploadTarget, partCount: number): Promise<MultipartUpload>;
22
+ /**
23
+ * Complete a multipart upload
24
+ */
25
+ completeMultipartUpload(uploadId: string, key: string, _parts: CompletePart[]): Promise<UploadComplete>;
26
+ /**
27
+ * Abort a multipart upload
28
+ */
29
+ abortMultipartUpload(uploadId: string, _key: string): Promise<void>;
30
+ /**
31
+ * Get Tus protocol endpoint configuration
32
+ */
33
+ getTusEndpoint(target: UploadTarget): Promise<TusUploadConfig>;
34
+ /**
35
+ * Delete a file
36
+ */
37
+ deleteFile(_bucket: string, key: string): Promise<void>;
38
+ /**
39
+ * Generate a download URL
40
+ */
41
+ getDownloadUrl(_bucket: string, key: string, _expiresInSec?: number): Promise<string>;
42
+ /**
43
+ * Ensure directory exists
44
+ */
45
+ private ensureDir;
46
+ /**
47
+ * Generate a simple ETag from content
48
+ */
49
+ private generateETag;
50
+ /**
51
+ * Clean up resources
52
+ */
53
+ close(): Promise<void>;
54
+ }
55
+ /**
56
+ * Create local provider from environment variables
57
+ */
58
+ export declare function createLocalProvider(): LocalUploadProvider;
59
+ //# sourceMappingURL=local.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/providers/local.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,eAAe,EACf,cAAc,EACd,cAAc,EACd,YAAY,EACb,MAAM,UAAU,CAAC;AAElB,qBAAa,mBAAoB,YAAW,cAAc;IACxD,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,cAAc,CAA2D;gBAErE,MAAM,EAAE,mBAAmB;IAcvC;;OAEG;IACH,OAAO,CAAC,QAAQ;IAQhB;;OAEG;IACG,qBAAqB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC;IAqB3E;;OAEG;IACG,qBAAqB,CAAC,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAmC9F;;OAEG;IACG,uBAAuB,CAC3B,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,YAAY,EAAE,GACrB,OAAO,CAAC,cAAc,CAAC;IAsC1B;;OAEG;IACG,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMzE;;OAEG;IACG,cAAc,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC;IAcpE;;OAEG;IACG,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW7D;;OAEG;IACG,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI3F;;OAEG;YACW,SAAS;IAUvB;;OAEG;IACH,OAAO,CAAC,YAAY;IAUpB;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAI7B;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,mBAAmB,CAUzD"}
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Local filesystem upload provider
3
+ * For development and testing
4
+ */
5
+ import { randomUUID } from "node:crypto";
6
+ import { mkdir, rm, writeFile } from "node:fs/promises";
7
+ import { join } from "node:path";
8
+ import { logger } from "@nebutra/logger";
9
+ export class LocalUploadProvider {
10
+ config;
11
+ pendingUploads;
12
+ constructor(config) {
13
+ this.config = {
14
+ uploadDir: config.uploadDir,
15
+ httpBaseUrl: config.httpBaseUrl,
16
+ maxConcurrency: config.maxConcurrency || 10,
17
+ };
18
+ this.pendingUploads = new Map();
19
+ logger.info("Local upload provider initialized", {
20
+ uploadDir: this.config.uploadDir,
21
+ httpBaseUrl: this.config.httpBaseUrl,
22
+ });
23
+ }
24
+ /**
25
+ * Build object key with tenant prefix and UUID
26
+ */
27
+ buildKey(target) {
28
+ const id = randomUUID();
29
+ if (target.tenantId) {
30
+ return `${target.tenantId}/${id}/${target.key.split("/").pop()}`;
31
+ }
32
+ return `${id}/${target.key.split("/").pop()}`;
33
+ }
34
+ /**
35
+ * Generate presigned URL for simple uploads
36
+ */
37
+ async createPresignedUpload(target) {
38
+ const key = this.buildKey(target);
39
+ // Ensure directory exists
40
+ await this.ensureDir(target.bucket);
41
+ const expiresIn = 3600; // 1 hour
42
+ logger.debug("Local presigned upload URL generated", { key, bucket: target.bucket });
43
+ return {
44
+ url: `${this.config.httpBaseUrl}/upload?bucket=${encodeURIComponent(target.bucket)}&key=${encodeURIComponent(key)}`,
45
+ method: "PUT",
46
+ headers: {
47
+ "Content-Type": target.contentType,
48
+ },
49
+ expiresAt: new Date(Date.now() + expiresIn * 1000),
50
+ uploadId: key,
51
+ };
52
+ }
53
+ /**
54
+ * Initiate multipart upload
55
+ */
56
+ async createMultipartUpload(target, partCount) {
57
+ const key = this.buildKey(target);
58
+ const uploadId = randomUUID();
59
+ // Ensure directory exists
60
+ await this.ensureDir(target.bucket);
61
+ // Initialize upload tracking
62
+ this.pendingUploads.set(uploadId, {
63
+ key,
64
+ parts: new Map(),
65
+ });
66
+ // Generate presigned URLs for each part
67
+ const parts = Array.from({ length: partCount }, (_, i) => ({
68
+ partNumber: i + 1,
69
+ presignedUrl: `${this.config.httpBaseUrl}/upload/multipart?bucket=${encodeURIComponent(target.bucket)}&uploadId=${encodeURIComponent(uploadId)}&partNumber=${i + 1}`,
70
+ }));
71
+ logger.info("Local multipart upload initiated", {
72
+ key,
73
+ bucket: target.bucket,
74
+ uploadId,
75
+ partCount,
76
+ });
77
+ return {
78
+ uploadId,
79
+ key,
80
+ bucket: target.bucket,
81
+ parts,
82
+ completeUrl: `${this.config.httpBaseUrl}/upload/multipart/complete?bucket=${encodeURIComponent(target.bucket)}&uploadId=${encodeURIComponent(uploadId)}`,
83
+ };
84
+ }
85
+ /**
86
+ * Complete a multipart upload
87
+ */
88
+ async completeMultipartUpload(uploadId, key, _parts) {
89
+ const upload = this.pendingUploads.get(uploadId);
90
+ if (!upload) {
91
+ throw new Error(`Upload not found: ${uploadId}`);
92
+ }
93
+ // For local storage, etag is a simple hash of part data
94
+ // In production, this would be the actual S3 etag
95
+ const filePath = join(this.config.uploadDir, key);
96
+ // Ensure directory exists
97
+ const dir = filePath.substring(0, filePath.lastIndexOf("/"));
98
+ await this.ensureDir(dir);
99
+ // Combine parts in order
100
+ const parts_array = Array.from(upload.parts.entries())
101
+ .sort(([a], [b]) => a - b)
102
+ .map(([, buffer]) => buffer);
103
+ const combined = Buffer.concat(parts_array);
104
+ await writeFile(filePath, combined);
105
+ this.pendingUploads.delete(uploadId);
106
+ logger.info("Local multipart upload completed", { key, uploadId, size: combined.length });
107
+ return {
108
+ key,
109
+ bucket: "local",
110
+ url: `${this.config.httpBaseUrl}/files/${key}`,
111
+ size: combined.length,
112
+ contentType: "application/octet-stream",
113
+ etag: this.generateETag(combined),
114
+ };
115
+ }
116
+ /**
117
+ * Abort a multipart upload
118
+ */
119
+ async abortMultipartUpload(uploadId, _key) {
120
+ this.pendingUploads.delete(uploadId);
121
+ logger.info("Local multipart upload aborted", { uploadId });
122
+ }
123
+ /**
124
+ * Get Tus protocol endpoint configuration
125
+ */
126
+ async getTusEndpoint(target) {
127
+ await this.ensureDir(target.bucket);
128
+ return {
129
+ endpoint: `${this.config.httpBaseUrl}/upload/tus`,
130
+ chunkSize: 1 * 1024 * 1024, // 1MB chunks for local testing
131
+ retryDelays: [0, 1000, 3000],
132
+ metadata: {
133
+ ...target.metadata,
134
+ filename: target.key,
135
+ },
136
+ };
137
+ }
138
+ /**
139
+ * Delete a file
140
+ */
141
+ async deleteFile(_bucket, key) {
142
+ const filePath = join(this.config.uploadDir, key);
143
+ try {
144
+ await rm(filePath, { force: true });
145
+ logger.debug("Local file deleted", { key });
146
+ }
147
+ catch (error) {
148
+ logger.warn("Failed to delete local file", { key, error });
149
+ }
150
+ }
151
+ /**
152
+ * Generate a download URL
153
+ */
154
+ async getDownloadUrl(_bucket, key, _expiresInSec) {
155
+ return `${this.config.httpBaseUrl}/files/${key}`;
156
+ }
157
+ /**
158
+ * Ensure directory exists
159
+ */
160
+ async ensureDir(dirPath) {
161
+ const fullPath = join(this.config.uploadDir, dirPath);
162
+ try {
163
+ await mkdir(fullPath, { recursive: true });
164
+ }
165
+ catch (error) {
166
+ logger.warn("Failed to create directory", { path: fullPath, error });
167
+ }
168
+ }
169
+ /**
170
+ * Generate a simple ETag from content
171
+ */
172
+ generateETag(buffer) {
173
+ // Simple hash implementation
174
+ let hash = 0;
175
+ for (let i = 0; i < buffer.length; i++) {
176
+ hash = (hash << 5) - hash + (buffer[i] ?? 0);
177
+ hash = hash & hash; // Convert to 32bit integer
178
+ }
179
+ return `"${Math.abs(hash).toString(16)}"`;
180
+ }
181
+ /**
182
+ * Clean up resources
183
+ */
184
+ async close() {
185
+ this.pendingUploads.clear();
186
+ logger.debug("Local upload provider closed");
187
+ }
188
+ }
189
+ /**
190
+ * Create local provider from environment variables
191
+ */
192
+ export function createLocalProvider() {
193
+ const config = {
194
+ uploadDir: process.env.UPLOAD_DIR || "/tmp/nebutra-uploads",
195
+ httpBaseUrl: process.env.UPLOAD_HTTP_BASE_URL || "http://localhost:3000",
196
+ maxConcurrency: process.env.UPLOAD_MAX_CONCURRENCY
197
+ ? parseInt(process.env.UPLOAD_MAX_CONCURRENCY, 10)
198
+ : 10,
199
+ };
200
+ return new LocalUploadProvider(config);
201
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * S3/R2 upload provider
3
+ * Supports both AWS S3 and Cloudflare R2 (S3-compatible)
4
+ */
5
+ import type { CompletePart, MultipartUpload, PresignedUpload, S3ProviderConfig, TusUploadConfig, UploadComplete, UploadProvider, UploadTarget } from "../types";
6
+ export declare class S3UploadProvider implements UploadProvider {
7
+ private client;
8
+ private config;
9
+ private uploadTracking;
10
+ constructor(config: S3ProviderConfig);
11
+ /**
12
+ * Build object key with tenant prefix
13
+ */
14
+ private buildKey;
15
+ /**
16
+ * Generate presigned PUT URL for simple uploads
17
+ */
18
+ createPresignedUpload(target: UploadTarget): Promise<PresignedUpload>;
19
+ /**
20
+ * Initiate multipart upload for large files
21
+ */
22
+ createMultipartUpload(target: UploadTarget, partCount: number): Promise<MultipartUpload>;
23
+ /**
24
+ * Generate a presigned URL for the complete multipart upload operation
25
+ */
26
+ private generateCompleteUrl;
27
+ /**
28
+ * Complete a multipart upload
29
+ */
30
+ completeMultipartUpload(uploadId: string, key: string, parts: CompletePart[]): Promise<UploadComplete>;
31
+ /**
32
+ * Abort a multipart upload
33
+ */
34
+ abortMultipartUpload(uploadId: string, key: string): Promise<void>;
35
+ /**
36
+ * Get Tus protocol endpoint configuration
37
+ */
38
+ getTusEndpoint(target: UploadTarget): Promise<TusUploadConfig>;
39
+ /**
40
+ * Delete a file from S3
41
+ */
42
+ deleteFile(bucket: string, key: string): Promise<void>;
43
+ /**
44
+ * Generate a presigned download URL
45
+ */
46
+ getDownloadUrl(bucket: string, key: string, expiresInSec?: number): Promise<string>;
47
+ /**
48
+ * Generate public CDN URL for assets
49
+ */
50
+ private generateDownloadUrl;
51
+ /**
52
+ * Clean up resources
53
+ */
54
+ close(): Promise<void>;
55
+ }
56
+ /**
57
+ * Create S3 provider from environment variables
58
+ */
59
+ export declare function createS3Provider(): S3UploadProvider;
60
+ //# sourceMappingURL=s3.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../../src/providers/s3.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAgBH,OAAO,KAAK,EACV,YAAY,EACZ,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,cAAc,EACd,YAAY,EACb,MAAM,UAAU,CAAC;AAElB,qBAAa,gBAAiB,YAAW,cAAc;IACrD,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,MAAM,CAOZ;IACF,OAAO,CAAC,cAAc,CAA+C;gBAEzD,MAAM,EAAE,gBAAgB;IAwBpC;;OAEG;IACH,OAAO,CAAC,QAAQ;IAQhB;;OAEG;IACG,qBAAqB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC;IA2B3E;;OAEG;IACG,qBAAqB,CAAC,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAgE9F;;OAEG;YACW,mBAAmB;IAUjC;;OAEG;IACG,uBAAuB,CAC3B,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,YAAY,EAAE,GACpB,OAAO,CAAC,cAAc,CAAC;IAuC1B;;OAEG;IACG,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBxE;;OAEG;IACG,cAAc,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC;IA4BpE;;OAEG;IACG,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW5D;;OAEG;IACG,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,GAAE,MAAa,GAAG,OAAO,CAAC,MAAM,CAAC;IAS/F;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAI3B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAK7B;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,gBAAgB,CAoBnD"}