@uploadista/data-store-gcs 0.0.3

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.
Files changed (41) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-check.log +5 -0
  3. package/LICENSE +21 -0
  4. package/README.md +479 -0
  5. package/dist/examples.d.ts +44 -0
  6. package/dist/examples.d.ts.map +1 -0
  7. package/dist/examples.js +82 -0
  8. package/dist/gcs-store-rest.d.ts +16 -0
  9. package/dist/gcs-store-rest.d.ts.map +1 -0
  10. package/dist/gcs-store-rest.js +188 -0
  11. package/dist/gcs-store-v2.d.ts +13 -0
  12. package/dist/gcs-store-v2.d.ts.map +1 -0
  13. package/dist/gcs-store-v2.js +190 -0
  14. package/dist/gcs-store.d.ts +12 -0
  15. package/dist/gcs-store.d.ts.map +1 -0
  16. package/dist/gcs-store.js +282 -0
  17. package/dist/index.d.ts +4 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +5 -0
  20. package/dist/services/gcs-client-nodejs.service.d.ts +4 -0
  21. package/dist/services/gcs-client-nodejs.service.d.ts.map +1 -0
  22. package/dist/services/gcs-client-nodejs.service.js +312 -0
  23. package/dist/services/gcs-client-rest.service.d.ts +4 -0
  24. package/dist/services/gcs-client-rest.service.d.ts.map +1 -0
  25. package/dist/services/gcs-client-rest.service.js +299 -0
  26. package/dist/services/gcs-client.service.d.ts +56 -0
  27. package/dist/services/gcs-client.service.d.ts.map +1 -0
  28. package/dist/services/gcs-client.service.js +3 -0
  29. package/dist/services/index.d.ts +4 -0
  30. package/dist/services/index.d.ts.map +1 -0
  31. package/dist/services/index.js +3 -0
  32. package/package.json +31 -0
  33. package/src/gcs-store-v2.ts +286 -0
  34. package/src/gcs-store.ts +398 -0
  35. package/src/index.ts +6 -0
  36. package/src/services/gcs-client-nodejs.service.ts +435 -0
  37. package/src/services/gcs-client-rest.service.ts +406 -0
  38. package/src/services/gcs-client.service.ts +117 -0
  39. package/src/services/index.ts +3 -0
  40. package/tsconfig.json +12 -0
  41. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,406 @@
1
+ import { UploadistaError } from "@uploadista/core/errors";
2
+ import { Effect, Layer } from "effect";
3
+ import {
4
+ type GCSClientConfig,
5
+ GCSClientService,
6
+ type GCSObjectMetadata,
7
+ type GCSOperationContext,
8
+ } from "./gcs-client.service";
9
+
10
+ function createRESTGCSClient(config: GCSClientConfig) {
11
+ if (!config.accessToken) {
12
+ throw new Error("accessToken is required for REST API implementation");
13
+ }
14
+
15
+ const baseUrl = `https://storage.googleapis.com/storage/v1/b/${config.bucket}`;
16
+ const uploadUrl = `https://storage.googleapis.com/upload/storage/v1/b/${config.bucket}/o`;
17
+ const accessToken = config.accessToken;
18
+
19
+ const getAuthHeaders = () => ({
20
+ Authorization: `Bearer ${accessToken}`,
21
+ "Content-Type": "application/json",
22
+ });
23
+
24
+ const getObject = (key: string) =>
25
+ Effect.tryPromise({
26
+ try: async () => {
27
+ const response = await fetch(
28
+ `${baseUrl}/o/${encodeURIComponent(key)}?alt=media`,
29
+ {
30
+ headers: {
31
+ Authorization: `Bearer ${accessToken}`,
32
+ },
33
+ },
34
+ );
35
+
36
+ if (!response.ok) {
37
+ if (response.status === 404) {
38
+ throw new Error("File not found");
39
+ }
40
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
41
+ }
42
+ if (!response.body) {
43
+ throw new Error("body not found");
44
+ }
45
+
46
+ return response.body;
47
+ },
48
+ catch: (error) => {
49
+ if (error instanceof Error && error.message.includes("not found")) {
50
+ return UploadistaError.fromCode("FILE_NOT_FOUND");
51
+ }
52
+ return UploadistaError.fromCode("UNKNOWN_ERROR", { cause: error });
53
+ },
54
+ });
55
+
56
+ const getObjectMetadata = (key: string) =>
57
+ Effect.tryPromise({
58
+ try: async () => {
59
+ const response = await fetch(
60
+ `${baseUrl}/o/${encodeURIComponent(key)}`,
61
+ {
62
+ headers: getAuthHeaders(),
63
+ },
64
+ );
65
+
66
+ if (!response.ok) {
67
+ if (response.status === 404) {
68
+ throw new Error("File not found");
69
+ }
70
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
71
+ }
72
+
73
+ const data = (await response.json()) as {
74
+ name: string;
75
+ bucket: string;
76
+ size?: string;
77
+ contentType?: string;
78
+ metadata?: Record<string, string>;
79
+ generation?: string;
80
+ timeCreated?: string;
81
+ updated?: string;
82
+ };
83
+
84
+ return {
85
+ name: data.name,
86
+ bucket: data.bucket,
87
+ size: data.size ? Number.parseInt(data.size, 10) : undefined,
88
+ contentType: data.contentType,
89
+ metadata: data.metadata || {},
90
+ generation: data.generation,
91
+ timeCreated: data.timeCreated,
92
+ updated: data.updated,
93
+ } as GCSObjectMetadata;
94
+ },
95
+ catch: (error) => {
96
+ if (error instanceof Error && error.message.includes("not found")) {
97
+ return UploadistaError.fromCode("FILE_NOT_FOUND");
98
+ }
99
+ return UploadistaError.fromCode("UNKNOWN_ERROR", { cause: error });
100
+ },
101
+ });
102
+
103
+ const objectExists = (key: string) =>
104
+ Effect.tryPromise({
105
+ try: async () => {
106
+ const response = await fetch(
107
+ `${baseUrl}/o/${encodeURIComponent(key)}`,
108
+ {
109
+ method: "HEAD",
110
+ headers: {
111
+ Authorization: `Bearer ${accessToken}`,
112
+ },
113
+ },
114
+ );
115
+
116
+ return response.ok;
117
+ },
118
+ catch: (error) => {
119
+ return UploadistaError.fromCode("UNKNOWN_ERROR", { cause: error });
120
+ },
121
+ });
122
+
123
+ const putObject = (
124
+ key: string,
125
+ body: Uint8Array,
126
+ context?: Partial<GCSOperationContext>,
127
+ ) =>
128
+ Effect.tryPromise({
129
+ try: async () => {
130
+ const metadata = {
131
+ name: key,
132
+ contentType: context?.contentType || "application/octet-stream",
133
+ metadata: context?.metadata || {},
134
+ };
135
+
136
+ const response = await fetch(
137
+ `${uploadUrl}?uploadType=media&name=${encodeURIComponent(key)}`,
138
+ {
139
+ method: "POST",
140
+ headers: {
141
+ Authorization: `Bearer ${accessToken}`,
142
+ "Content-Type": metadata.contentType,
143
+ "Content-Length": body.length.toString(),
144
+ },
145
+ body: body,
146
+ },
147
+ );
148
+
149
+ if (!response.ok) {
150
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
151
+ }
152
+
153
+ return key;
154
+ },
155
+ catch: (error) => {
156
+ return UploadistaError.fromCode("FILE_WRITE_ERROR", { cause: error });
157
+ },
158
+ });
159
+
160
+ const deleteObject = (key: string) =>
161
+ Effect.tryPromise({
162
+ try: async () => {
163
+ const response = await fetch(
164
+ `${baseUrl}/o/${encodeURIComponent(key)}`,
165
+ {
166
+ method: "DELETE",
167
+ headers: {
168
+ Authorization: `Bearer ${accessToken}`,
169
+ },
170
+ },
171
+ );
172
+
173
+ // 404 is OK - object didn't exist
174
+ if (!response.ok && response.status !== 404) {
175
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
176
+ }
177
+ },
178
+ catch: (error) => {
179
+ return UploadistaError.fromCode("UNKNOWN_ERROR", { cause: error });
180
+ },
181
+ });
182
+
183
+ const createResumableUpload = (context: GCSOperationContext) =>
184
+ Effect.tryPromise({
185
+ try: async () => {
186
+ const metadata = {
187
+ name: context.key,
188
+ contentType: context.contentType || "application/octet-stream",
189
+ metadata: context.metadata || {},
190
+ };
191
+
192
+ const response = await fetch(
193
+ `${uploadUrl}?uploadType=resumable&name=${encodeURIComponent(context.key)}`,
194
+ {
195
+ method: "POST",
196
+ headers: {
197
+ Authorization: `Bearer ${accessToken}`,
198
+ "Content-Type": "application/json",
199
+ },
200
+ body: JSON.stringify(metadata),
201
+ },
202
+ );
203
+
204
+ if (!response.ok) {
205
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
206
+ }
207
+
208
+ const resumableUploadUrl = response.headers.get("Location");
209
+ if (!resumableUploadUrl) {
210
+ throw new Error("No upload URL returned");
211
+ }
212
+
213
+ return resumableUploadUrl;
214
+ },
215
+ catch: (error) => {
216
+ return UploadistaError.fromCode("FILE_WRITE_ERROR", { cause: error });
217
+ },
218
+ });
219
+
220
+ const uploadChunk = (
221
+ uploadUrl: string,
222
+ chunk: Uint8Array,
223
+ start: number,
224
+ total?: number,
225
+ ) =>
226
+ Effect.tryPromise({
227
+ try: async () => {
228
+ const end = start + chunk.length - 1;
229
+ const contentRange = total
230
+ ? `bytes ${start}-${end}/${total}`
231
+ : `bytes ${start}-${end}/*`;
232
+
233
+ const response = await fetch(uploadUrl, {
234
+ method: "PUT",
235
+ headers: {
236
+ "Content-Length": chunk.length.toString(),
237
+ "Content-Range": contentRange,
238
+ },
239
+ body: chunk,
240
+ });
241
+
242
+ // 308 means more data needed, 200/201 means complete
243
+ const completed = response.status === 200 || response.status === 201;
244
+
245
+ if (!completed && response.status !== 308) {
246
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
247
+ }
248
+
249
+ return {
250
+ completed,
251
+ bytesUploaded: end + 1,
252
+ };
253
+ },
254
+ catch: (error) => {
255
+ return UploadistaError.fromCode("FILE_WRITE_ERROR", { cause: error });
256
+ },
257
+ });
258
+
259
+ const getUploadStatus = (uploadUrl: string) =>
260
+ Effect.tryPromise({
261
+ try: async () => {
262
+ const response = await fetch(uploadUrl, {
263
+ method: "PUT",
264
+ headers: {
265
+ "Content-Range": "bytes */*",
266
+ },
267
+ });
268
+
269
+ if (response.status === 308) {
270
+ // Upload incomplete
271
+ const range = response.headers.get("Range");
272
+ const bytesUploaded = range
273
+ ? Number.parseInt(range.split("-")[1], 10) + 1
274
+ : 0;
275
+
276
+ return {
277
+ bytesUploaded,
278
+ completed: false,
279
+ };
280
+ } else if (response.status === 200 || response.status === 201) {
281
+ // Upload complete
282
+ return {
283
+ bytesUploaded: 0, // We don't know the exact size
284
+ completed: true,
285
+ };
286
+ } else {
287
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
288
+ }
289
+ },
290
+ catch: (error) => {
291
+ return UploadistaError.fromCode("UNKNOWN_ERROR", { cause: error });
292
+ },
293
+ });
294
+
295
+ const cancelUpload = (uploadUrl: string) =>
296
+ Effect.tryPromise({
297
+ try: async () => {
298
+ // Cancel by sending DELETE to upload URL
299
+ await fetch(uploadUrl, {
300
+ method: "DELETE",
301
+ });
302
+ },
303
+ catch: (error) => {
304
+ return UploadistaError.fromCode("UNKNOWN_ERROR", { cause: error });
305
+ },
306
+ });
307
+
308
+ const composeObjects = (
309
+ sourceKeys: string[],
310
+ destinationKey: string,
311
+ context?: Partial<GCSOperationContext>,
312
+ ) =>
313
+ Effect.tryPromise({
314
+ try: async () => {
315
+ const composeRequest = {
316
+ kind: "storage#composeRequest",
317
+ sourceObjects: sourceKeys.map((key) => ({ name: key })),
318
+ destination: {
319
+ name: destinationKey,
320
+ contentType: context?.contentType || "application/octet-stream",
321
+ metadata: context?.metadata || {},
322
+ },
323
+ };
324
+
325
+ const response = await fetch(
326
+ `${baseUrl}/o/${encodeURIComponent(destinationKey)}/compose`,
327
+ {
328
+ method: "POST",
329
+ headers: getAuthHeaders(),
330
+ body: JSON.stringify(composeRequest),
331
+ },
332
+ );
333
+
334
+ if (!response.ok) {
335
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
336
+ }
337
+
338
+ return destinationKey;
339
+ },
340
+ catch: (error) => {
341
+ return UploadistaError.fromCode("FILE_WRITE_ERROR", { cause: error });
342
+ },
343
+ });
344
+
345
+ const putTemporaryObject = (
346
+ key: string,
347
+ body: Uint8Array,
348
+ context?: Partial<GCSOperationContext>,
349
+ ) => putObject(`${key}_tmp`, body, context);
350
+
351
+ const getTemporaryObject = (key: string) =>
352
+ Effect.tryPromise({
353
+ try: async () => {
354
+ try {
355
+ return await getObject(`${key}_tmp`).pipe(Effect.runPromise);
356
+ } catch {
357
+ return undefined;
358
+ }
359
+ },
360
+ catch: () => {
361
+ return UploadistaError.fromCode("UNKNOWN_ERROR");
362
+ },
363
+ });
364
+
365
+ const deleteTemporaryObject = (key: string) => deleteObject(`${key}_tmp`);
366
+
367
+ const getObjectBuffer = (key: string) =>
368
+ Effect.tryPromise({
369
+ try: async () => {
370
+ const response = await fetch(
371
+ `${baseUrl}/o/${encodeURIComponent(key)}?alt=media`,
372
+ {
373
+ headers: getAuthHeaders(),
374
+ },
375
+ );
376
+ if (!response.ok) {
377
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
378
+ }
379
+ return new Uint8Array(await response.arrayBuffer());
380
+ },
381
+ catch: (error) => {
382
+ return UploadistaError.fromCode("FILE_READ_ERROR", { cause: error });
383
+ },
384
+ });
385
+
386
+ return {
387
+ bucket: config.bucket,
388
+ getObject,
389
+ getObjectBuffer,
390
+ getObjectMetadata,
391
+ objectExists,
392
+ putObject,
393
+ deleteObject,
394
+ createResumableUpload,
395
+ uploadChunk,
396
+ getUploadStatus,
397
+ cancelUpload,
398
+ composeObjects,
399
+ putTemporaryObject,
400
+ getTemporaryObject,
401
+ deleteTemporaryObject,
402
+ };
403
+ }
404
+
405
+ export const GCSClientRESTLayer = (config: GCSClientConfig) =>
406
+ Layer.succeed(GCSClientService, createRESTGCSClient(config));
@@ -0,0 +1,117 @@
1
+ import type { UploadistaError } from "@uploadista/core/errors";
2
+ import { Context, type Effect } from "effect";
3
+
4
+ export interface GCSOperationContext {
5
+ bucket: string;
6
+ key: string;
7
+ contentType?: string;
8
+ metadata?: Record<string, string | null>;
9
+ }
10
+
11
+ export interface GCSObjectMetadata {
12
+ name: string;
13
+ bucket: string;
14
+ size?: number;
15
+ contentType?: string;
16
+ metadata?: Record<string, string | null>;
17
+ generation?: string;
18
+ timeCreated?: string;
19
+ updated?: string;
20
+ }
21
+
22
+ export type GCSClient = {
23
+ readonly bucket: string;
24
+
25
+ // Basic GCS operations
26
+ readonly getObject: (
27
+ key: string,
28
+ ) => Effect.Effect<ReadableStream, UploadistaError>;
29
+ readonly getObjectMetadata: (
30
+ key: string,
31
+ ) => Effect.Effect<GCSObjectMetadata, UploadistaError>;
32
+ readonly getObjectBuffer: (
33
+ key: string,
34
+ ) => Effect.Effect<Uint8Array, UploadistaError>;
35
+ readonly objectExists: (
36
+ key: string,
37
+ ) => Effect.Effect<boolean, UploadistaError>;
38
+ readonly putObject: (
39
+ key: string,
40
+ body: Uint8Array,
41
+ context?: Partial<GCSOperationContext>,
42
+ ) => Effect.Effect<string, UploadistaError>;
43
+ readonly putObjectFromStream?: (
44
+ key: string,
45
+ offset: number,
46
+ readableStream: ReadableStream,
47
+ context?: Partial<GCSOperationContext>,
48
+ onProgress?: (chunkSize: number) => void, // Called with incremental bytes per chunk
49
+ ) => Effect.Effect<number, UploadistaError>;
50
+ readonly putObjectFromStreamWithPatching?: (
51
+ key: string,
52
+ offset: number,
53
+ readableStream: ReadableStream,
54
+ context?: Partial<GCSOperationContext>,
55
+ onProgress?: (chunkSize: number) => void, // Called with incremental bytes per chunk
56
+ isAppend?: boolean,
57
+ ) => Effect.Effect<number, UploadistaError>;
58
+ readonly deleteObject: (key: string) => Effect.Effect<void, UploadistaError>;
59
+
60
+ // Resumable upload operations
61
+ readonly createResumableUpload: (
62
+ context: GCSOperationContext,
63
+ ) => Effect.Effect<string, UploadistaError>; // Returns upload URL
64
+ readonly uploadChunk: (
65
+ uploadUrl: string,
66
+ chunk: Uint8Array,
67
+ start: number,
68
+ total?: number,
69
+ ) => Effect.Effect<
70
+ { completed: boolean; bytesUploaded: number },
71
+ UploadistaError
72
+ >;
73
+ readonly getUploadStatus: (
74
+ uploadUrl: string,
75
+ ) => Effect.Effect<
76
+ { bytesUploaded: number; completed: boolean },
77
+ UploadistaError
78
+ >;
79
+ readonly cancelUpload: (
80
+ uploadUrl: string,
81
+ ) => Effect.Effect<void, UploadistaError>;
82
+
83
+ // Compose operations (GCS specific - for combining files)
84
+ readonly composeObjects: (
85
+ sourceKeys: string[],
86
+ destinationKey: string,
87
+ context?: Partial<GCSOperationContext>,
88
+ ) => Effect.Effect<string, UploadistaError>;
89
+
90
+ // Temporary file operations (for patches)
91
+ readonly putTemporaryObject: (
92
+ key: string,
93
+ body: Uint8Array,
94
+ context?: Partial<GCSOperationContext>,
95
+ ) => Effect.Effect<string, UploadistaError>;
96
+ readonly getTemporaryObject: (
97
+ key: string,
98
+ ) => Effect.Effect<ReadableStream | undefined, UploadistaError>;
99
+ readonly deleteTemporaryObject: (
100
+ key: string,
101
+ ) => Effect.Effect<void, UploadistaError>;
102
+ };
103
+
104
+ export class GCSClientService extends Context.Tag("GCSClientService")<
105
+ GCSClientService,
106
+ GCSClient
107
+ >() {}
108
+
109
+ export interface GCSClientConfig {
110
+ bucket: string;
111
+ // For Node.js implementation
112
+ keyFilename?: string;
113
+ credentials?: object;
114
+ projectId?: string;
115
+ // For REST API implementation
116
+ accessToken?: string;
117
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./gcs-client.service";
2
+ export * from "./gcs-client-nodejs.service";
3
+ export * from "./gcs-client-rest.service";
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "@uploadista/typescript-config/server.json",
3
+ "compilerOptions": {
4
+ "baseUrl": "./",
5
+ "paths": {
6
+ "@/*": ["./src/*"]
7
+ },
8
+ "outDir": "./dist",
9
+ "rootDir": "./src"
10
+ },
11
+ "include": ["src"]
12
+ }
@@ -0,0 +1 @@
1
+ {"root":["./src/gcs-store-v2.ts","./src/gcs-store.ts","./src/index.ts","./src/services/gcs-client-nodejs.service.ts","./src/services/gcs-client-rest.service.ts","./src/services/gcs-client.service.ts","./src/services/index.ts"],"version":"5.9.3"}