@directus/storage-driver-supabase 3.0.8 → 3.0.10

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 (3) hide show
  1. package/dist/index.d.ts +43 -42
  2. package/dist/index.js +192 -211
  3. package/package.json +11 -11
package/dist/index.d.ts CHANGED
@@ -1,47 +1,48 @@
1
- import { TusDriver } from '@directus/storage';
2
- import { ReadOptions, ChunkedUploadContext } from '@directus/types';
3
- import { Readable } from 'node:stream';
1
+ import { Readable } from "node:stream";
2
+ import { TusDriver } from "@directus/storage";
3
+ import { ChunkedUploadContext, ReadOptions } from "@directus/types";
4
4
 
5
+ //#region src/index.d.ts
5
6
  type DriverSupabaseConfig = {
6
- bucket: string;
7
- serviceRole: string;
8
- projectId?: string;
9
- /** Allows a custom Supabase endpoint for self-hosting */
10
- endpoint?: string;
11
- root?: string;
12
- tus?: {
13
- chunkSize?: number;
14
- };
7
+ bucket: string;
8
+ serviceRole: string;
9
+ projectId?: string;
10
+ /** Allows a custom Supabase endpoint for self-hosting */
11
+ endpoint?: string;
12
+ root?: string;
13
+ tus?: {
14
+ chunkSize?: number;
15
+ };
15
16
  };
16
17
  declare class DriverSupabase implements TusDriver {
17
- private config;
18
- private client;
19
- private bucket;
20
- private readonly preferredChunkSize;
21
- constructor(config: DriverSupabaseConfig);
22
- private get endpoint();
23
- private getClient;
24
- private getBucket;
25
- private fullPath;
26
- private getAuthenticatedUrl;
27
- private getResumableUrl;
28
- read(filepath: string, options?: ReadOptions): Promise<Readable>;
29
- stat(filepath: string): Promise<{
30
- size: any;
31
- modified: Date;
32
- }>;
33
- exists(filepath: string): Promise<boolean>;
34
- move(src: string, dest: string): Promise<void>;
35
- copy(src: string, dest: string): Promise<void>;
36
- write(filepath: string, content: Readable, type?: string): Promise<void>;
37
- delete(filepath: string): Promise<void>;
38
- list(prefix?: string): AsyncIterable<string>;
39
- listGenerator(prefix: string): AsyncIterable<string>;
40
- get tusExtensions(): string[];
41
- createChunkedUpload(_filepath: string, context: ChunkedUploadContext): Promise<ChunkedUploadContext>;
42
- writeChunk(filepath: string, content: Readable, offset: number, context: ChunkedUploadContext): Promise<number>;
43
- finishChunkedUpload(_filepath: string, _context: ChunkedUploadContext): Promise<void>;
44
- deleteChunkedUpload(filepath: string, _context: ChunkedUploadContext): Promise<void>;
18
+ private config;
19
+ private client;
20
+ private bucket;
21
+ private readonly preferredChunkSize;
22
+ constructor(config: DriverSupabaseConfig);
23
+ private get endpoint();
24
+ private getClient;
25
+ private getBucket;
26
+ private fullPath;
27
+ private getAuthenticatedUrl;
28
+ private getResumableUrl;
29
+ read(filepath: string, options?: ReadOptions): Promise<Readable>;
30
+ stat(filepath: string): Promise<{
31
+ size: any;
32
+ modified: Date;
33
+ }>;
34
+ exists(filepath: string): Promise<boolean>;
35
+ move(src: string, dest: string): Promise<void>;
36
+ copy(src: string, dest: string): Promise<void>;
37
+ write(filepath: string, content: Readable, type?: string): Promise<void>;
38
+ delete(filepath: string): Promise<void>;
39
+ list(prefix?: string): AsyncIterable<string>;
40
+ listGenerator(prefix: string): AsyncIterable<string>;
41
+ get tusExtensions(): string[];
42
+ createChunkedUpload(_filepath: string, context: ChunkedUploadContext): Promise<ChunkedUploadContext>;
43
+ writeChunk(filepath: string, content: Readable, offset: number, context: ChunkedUploadContext): Promise<number>;
44
+ finishChunkedUpload(_filepath: string, _context: ChunkedUploadContext): Promise<void>;
45
+ deleteChunkedUpload(filepath: string, _context: ChunkedUploadContext): Promise<void>;
45
46
  }
46
-
47
- export { DriverSupabase, type DriverSupabaseConfig, DriverSupabase as default };
47
+ //#endregion
48
+ export { DriverSupabase, DriverSupabase as default, DriverSupabaseConfig };
package/dist/index.js CHANGED
@@ -1,223 +1,204 @@
1
- // src/index.ts
2
1
  import { DEFAULT_CHUNK_SIZE } from "@directus/constants";
3
2
  import { normalizePath } from "@directus/utils";
4
3
  import { StorageClient } from "@supabase/storage-js";
5
- import { join } from "path";
6
- import { Readable } from "stream";
4
+ import { join } from "node:path";
5
+ import { Readable } from "node:stream";
7
6
  import * as tus from "tus-js-client";
8
7
  import { fetch } from "undici";
8
+
9
+ //#region src/index.ts
9
10
  var DriverSupabase = class {
10
- config;
11
- client;
12
- bucket;
13
- // TUS specific members
14
- preferredChunkSize;
15
- constructor(config) {
16
- this.config = {
17
- ...config,
18
- root: normalizePath(config.root ?? "", { removeLeading: true })
19
- };
20
- this.preferredChunkSize = this.config.tus?.chunkSize ?? DEFAULT_CHUNK_SIZE;
21
- this.client = this.getClient();
22
- this.bucket = this.getBucket();
23
- }
24
- get endpoint() {
25
- return this.config.endpoint ?? `https://${this.config.projectId}.supabase.co/storage/v1`;
26
- }
27
- getClient() {
28
- if (!this.config.projectId && !this.config.endpoint) {
29
- throw new Error("`project_id` or `endpoint` is required");
30
- }
31
- if (!this.config.serviceRole) {
32
- throw new Error("`service_role` is required");
33
- }
34
- return new StorageClient(this.endpoint, {
35
- apikey: this.config.serviceRole,
36
- Authorization: `Bearer ${this.config.serviceRole}`
37
- });
38
- }
39
- getBucket() {
40
- if (!this.config.bucket) {
41
- throw new Error("`bucket` is required");
42
- }
43
- return this.client.from(this.config.bucket);
44
- }
45
- fullPath(filepath) {
46
- const path = join(this.config.root, filepath);
47
- if (path === ".") return "";
48
- return normalizePath(path);
49
- }
50
- getAuthenticatedUrl(filepath) {
51
- return `${this.endpoint}/${join("object/authenticated", this.config.bucket, this.fullPath(filepath))}`;
52
- }
53
- getResumableUrl() {
54
- return `${this.endpoint}/upload/resumable`;
55
- }
56
- async read(filepath, options) {
57
- const { range } = options || {};
58
- const requestInit = { method: "GET" };
59
- requestInit.headers = {
60
- Authorization: `Bearer ${this.config.serviceRole}`
61
- };
62
- if (range) {
63
- requestInit.headers["Range"] = `bytes=${range.start ?? ""}-${range.end ?? ""}`;
64
- }
65
- const response = await fetch(this.getAuthenticatedUrl(filepath), requestInit);
66
- if (response.status >= 400 || !response.body) {
67
- throw new Error(`No stream returned for file "${filepath}"`);
68
- }
69
- return Readable.fromWeb(response.body);
70
- }
71
- async stat(filepath) {
72
- const { data, error } = await this.bucket.list(this.config.root, {
73
- search: filepath,
74
- limit: 1
75
- });
76
- if (error || data.length === 0) {
77
- throw new Error("File not found");
78
- }
79
- return {
80
- size: data[0]?.metadata["contentLength"] ?? 0,
81
- modified: new Date(data[0]?.metadata["lastModified"] || null)
82
- };
83
- }
84
- async exists(filepath) {
85
- try {
86
- await this.stat(filepath);
87
- return true;
88
- } catch {
89
- return false;
90
- }
91
- }
92
- async move(src, dest) {
93
- await this.bucket.move(this.fullPath(src), this.fullPath(dest));
94
- }
95
- async copy(src, dest) {
96
- await this.bucket.copy(this.fullPath(src), this.fullPath(dest));
97
- }
98
- async write(filepath, content, type) {
99
- await this.bucket.upload(this.fullPath(filepath), content, {
100
- contentType: type ?? "",
101
- cacheControl: "3600",
102
- upsert: true,
103
- duplex: "half"
104
- });
105
- }
106
- async delete(filepath) {
107
- await this.bucket.remove([this.fullPath(filepath)]);
108
- }
109
- list(prefix = "") {
110
- const fullPrefix = this.fullPath(prefix);
111
- return this.listGenerator(fullPrefix);
112
- }
113
- async *listGenerator(prefix) {
114
- const limit = 1e3;
115
- let offset = 0;
116
- let itemCount = 0;
117
- const isDirectory = prefix.endsWith("/");
118
- const prefixDirectory = isDirectory ? prefix : dirname(prefix);
119
- const search = isDirectory ? "" : prefix.split("/").pop() ?? "";
120
- do {
121
- const { data, error } = await this.bucket.list(prefixDirectory, {
122
- limit,
123
- offset,
124
- search
125
- });
126
- if (!data || error) {
127
- break;
128
- }
129
- itemCount = data.length;
130
- offset += itemCount;
131
- for (const item of data) {
132
- const filePath = normalizePath(join(prefixDirectory, item.name));
133
- if (item.id !== null) {
134
- yield filePath.substring(this.config.root ? this.config.root.length + 1 : 0);
135
- } else {
136
- yield* this.listGenerator(`${filePath}/`);
137
- }
138
- }
139
- } while (itemCount === limit);
140
- }
141
- get tusExtensions() {
142
- return ["creation", "termination", "expiration"];
143
- }
144
- async createChunkedUpload(_filepath, context) {
145
- return context;
146
- }
147
- async writeChunk(filepath, content, offset, context) {
148
- let bytesUploaded = offset || 0;
149
- const metadata = {
150
- bucketName: this.config.bucket,
151
- objectName: this.fullPath(filepath),
152
- contentType: context.metadata["type"] ?? "image/png",
153
- cacheControl: "3600"
154
- };
155
- await new Promise((resolve, reject) => {
156
- const upload = new tus.Upload(content, {
157
- endpoint: this.getResumableUrl(),
158
- // @ts-expect-error
159
- fileReader: new FileReader(),
160
- headers: {
161
- Authorization: `Bearer ${this.config.serviceRole}`,
162
- "x-upsert": "true"
163
- },
164
- metadata,
165
- chunkSize: this.preferredChunkSize,
166
- uploadSize: context.size,
167
- retryDelays: null,
168
- onError(error) {
169
- reject(error);
170
- },
171
- onChunkComplete: function(chunkSize) {
172
- bytesUploaded += chunkSize;
173
- resolve(null);
174
- },
175
- onSuccess() {
176
- resolve(null);
177
- },
178
- onUploadUrlAvailable() {
179
- if (!context.metadata["upload-url"]) {
180
- context.metadata["upload-url"] = upload.url;
181
- }
182
- }
183
- });
184
- if (context.metadata["upload-url"]) {
185
- upload.resumeFromPreviousUpload({
186
- size: context.size,
187
- creationTime: context.metadata["creation_date"],
188
- metadata,
189
- uploadUrl: context.metadata["upload-url"]
190
- });
191
- }
192
- upload.start();
193
- });
194
- return bytesUploaded;
195
- }
196
- async finishChunkedUpload(_filepath, _context) {
197
- }
198
- async deleteChunkedUpload(filepath, _context) {
199
- await this.delete(filepath);
200
- }
11
+ config;
12
+ client;
13
+ bucket;
14
+ preferredChunkSize;
15
+ constructor(config) {
16
+ this.config = {
17
+ ...config,
18
+ root: normalizePath(config.root ?? "", { removeLeading: true })
19
+ };
20
+ this.preferredChunkSize = this.config.tus?.chunkSize ?? DEFAULT_CHUNK_SIZE;
21
+ this.client = this.getClient();
22
+ this.bucket = this.getBucket();
23
+ }
24
+ get endpoint() {
25
+ return this.config.endpoint ?? `https://${this.config.projectId}.supabase.co/storage/v1`;
26
+ }
27
+ getClient() {
28
+ if (!this.config.projectId && !this.config.endpoint) throw new Error("`project_id` or `endpoint` is required");
29
+ if (!this.config.serviceRole) throw new Error("`service_role` is required");
30
+ return new StorageClient(this.endpoint, {
31
+ apikey: this.config.serviceRole,
32
+ Authorization: `Bearer ${this.config.serviceRole}`
33
+ });
34
+ }
35
+ getBucket() {
36
+ if (!this.config.bucket) throw new Error("`bucket` is required");
37
+ return this.client.from(this.config.bucket);
38
+ }
39
+ fullPath(filepath) {
40
+ const path = join(this.config.root, filepath);
41
+ if (path === ".") return "";
42
+ return normalizePath(path);
43
+ }
44
+ getAuthenticatedUrl(filepath) {
45
+ return `${this.endpoint}/${join("object/authenticated", this.config.bucket, this.fullPath(filepath))}`;
46
+ }
47
+ getResumableUrl() {
48
+ return `${this.endpoint}/upload/resumable`;
49
+ }
50
+ async read(filepath, options) {
51
+ const { range } = options || {};
52
+ const requestInit = { method: "GET" };
53
+ requestInit.headers = { Authorization: `Bearer ${this.config.serviceRole}` };
54
+ if (range) requestInit.headers["Range"] = `bytes=${range.start ?? ""}-${range.end ?? ""}`;
55
+ const response = await fetch(this.getAuthenticatedUrl(filepath), requestInit);
56
+ if (response.status >= 400 || !response.body) throw new Error(`No stream returned for file "${filepath}"`);
57
+ return Readable.fromWeb(response.body);
58
+ }
59
+ async stat(filepath) {
60
+ const { data, error } = await this.bucket.list(this.config.root, {
61
+ search: filepath,
62
+ limit: 1
63
+ });
64
+ if (error || data.length === 0) throw new Error("File not found");
65
+ return {
66
+ size: data[0]?.metadata["contentLength"] ?? 0,
67
+ modified: new Date(data[0]?.metadata["lastModified"] || null)
68
+ };
69
+ }
70
+ async exists(filepath) {
71
+ try {
72
+ await this.stat(filepath);
73
+ return true;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+ async move(src, dest) {
79
+ await this.bucket.move(this.fullPath(src), this.fullPath(dest));
80
+ }
81
+ async copy(src, dest) {
82
+ await this.bucket.copy(this.fullPath(src), this.fullPath(dest));
83
+ }
84
+ async write(filepath, content, type) {
85
+ const { error } = await this.bucket.upload(this.fullPath(filepath), content, {
86
+ contentType: type ?? "",
87
+ cacheControl: "3600",
88
+ upsert: true,
89
+ duplex: "half"
90
+ });
91
+ if (error) throw new Error(`Error uploading file "${filepath}"`, { cause: error });
92
+ }
93
+ async delete(filepath) {
94
+ await this.bucket.remove([this.fullPath(filepath)]);
95
+ }
96
+ list(prefix = "") {
97
+ const fullPrefix = this.fullPath(prefix);
98
+ return this.listGenerator(fullPrefix);
99
+ }
100
+ async *listGenerator(prefix) {
101
+ const limit = 1e3;
102
+ let offset = 0;
103
+ let itemCount = 0;
104
+ const isDirectory = prefix.endsWith("/");
105
+ const prefixDirectory = isDirectory ? prefix : dirname(prefix);
106
+ const search = isDirectory ? "" : prefix.split("/").pop() ?? "";
107
+ do {
108
+ const { data, error } = await this.bucket.list(prefixDirectory, {
109
+ limit,
110
+ offset,
111
+ search
112
+ });
113
+ if (!data || error) break;
114
+ itemCount = data.length;
115
+ offset += itemCount;
116
+ for (const item of data) {
117
+ const filePath = normalizePath(join(prefixDirectory, item.name));
118
+ if (item.id !== null) yield filePath.substring(this.config.root ? this.config.root.length + 1 : 0);
119
+ else yield* this.listGenerator(`${filePath}/`);
120
+ }
121
+ } while (itemCount === limit);
122
+ }
123
+ get tusExtensions() {
124
+ return [
125
+ "creation",
126
+ "termination",
127
+ "expiration"
128
+ ];
129
+ }
130
+ async createChunkedUpload(_filepath, context) {
131
+ return context;
132
+ }
133
+ async writeChunk(filepath, content, offset, context) {
134
+ let bytesUploaded = offset || 0;
135
+ const metadata = {
136
+ bucketName: this.config.bucket,
137
+ objectName: this.fullPath(filepath),
138
+ contentType: context.metadata["type"] ?? "image/png",
139
+ cacheControl: "3600"
140
+ };
141
+ await new Promise((resolve, reject) => {
142
+ const upload = new tus.Upload(content, {
143
+ endpoint: this.getResumableUrl(),
144
+ fileReader: new FileReader(),
145
+ headers: {
146
+ Authorization: `Bearer ${this.config.serviceRole}`,
147
+ "x-upsert": "true"
148
+ },
149
+ metadata,
150
+ chunkSize: this.preferredChunkSize,
151
+ uploadSize: context.size,
152
+ retryDelays: null,
153
+ onError(error) {
154
+ reject(error);
155
+ },
156
+ onChunkComplete: function(chunkSize) {
157
+ bytesUploaded += chunkSize;
158
+ resolve(null);
159
+ },
160
+ onSuccess() {
161
+ resolve(null);
162
+ },
163
+ onUploadUrlAvailable() {
164
+ if (!context.metadata["upload-url"]) context.metadata["upload-url"] = upload.url;
165
+ }
166
+ });
167
+ if (context.metadata["upload-url"]) upload.resumeFromPreviousUpload({
168
+ size: context.size,
169
+ creationTime: context.metadata["creation_date"],
170
+ metadata,
171
+ uploadUrl: context.metadata["upload-url"]
172
+ });
173
+ upload.start();
174
+ });
175
+ return bytesUploaded;
176
+ }
177
+ async finishChunkedUpload(_filepath, _context) {}
178
+ async deleteChunkedUpload(filepath, _context) {
179
+ await this.delete(filepath);
180
+ }
201
181
  };
202
- var index_default = DriverSupabase;
182
+ var src_default = DriverSupabase;
183
+ /**
184
+ * dirname implementation that always uses '/' to split and returns '' in case of no separator present.
185
+ */
203
186
  function dirname(path) {
204
- return path.split("/").slice(0, -1).join("/");
187
+ return path.split("/").slice(0, -1).join("/");
205
188
  }
206
- var StreamSource2 = class extends tus.StreamSource {
207
- _streamEnded = false;
208
- // @ts-expect-error
209
- async slice(start, end) {
210
- if (this._streamEnded) return null;
211
- this._streamEnded = true;
212
- return super.slice(0, end - start);
213
- }
189
+ var StreamSource = class extends tus.StreamSource {
190
+ _streamEnded = false;
191
+ async slice(start, end) {
192
+ if (this._streamEnded) return null;
193
+ this._streamEnded = true;
194
+ return super.slice(0, end - start);
195
+ }
214
196
  };
215
197
  var FileReader = class {
216
- async openFile(input, _) {
217
- return new StreamSource2(input);
218
- }
219
- };
220
- export {
221
- DriverSupabase,
222
- index_default as default
198
+ async openFile(input, _) {
199
+ return new StreamSource(input);
200
+ }
223
201
  };
202
+
203
+ //#endregion
204
+ export { DriverSupabase, src_default as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/storage-driver-supabase",
3
- "version": "3.0.8",
3
+ "version": "3.0.10",
4
4
  "description": "Supabase file storage abstraction for `@directus/storage`",
5
5
  "homepage": "https://directus.io",
6
6
  "repository": {
@@ -21,25 +21,25 @@
21
21
  "dist"
22
22
  ],
23
23
  "dependencies": {
24
- "@supabase/storage-js": "2.10.4",
24
+ "@supabase/storage-js": "2.76.1",
25
25
  "tus-js-client": "4.3.1",
26
- "undici": "7.13.0",
27
- "@directus/constants": "13.0.2",
28
- "@directus/storage": "12.0.1",
29
- "@directus/utils": "13.0.9"
26
+ "undici": "7.16.0",
27
+ "@directus/constants": "14.0.0",
28
+ "@directus/storage": "12.0.3",
29
+ "@directus/utils": "13.0.11"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@directus/tsconfig": "3.0.0",
33
33
  "@ngneat/falso": "8.0.2",
34
34
  "@vitest/coverage-v8": "3.2.4",
35
- "tsup": "8.5.0",
36
- "typescript": "5.8.3",
35
+ "tsdown": "0.15.11",
36
+ "typescript": "5.9.3",
37
37
  "vitest": "3.2.4",
38
- "@directus/types": "13.2.1"
38
+ "@directus/types": "13.3.0"
39
39
  },
40
40
  "scripts": {
41
- "build": "tsup src/index.ts --format=esm --dts",
42
- "dev": "tsup src/index.ts --format=esm --dts --watch",
41
+ "build": "tsdown src/index.ts --dts",
42
+ "dev": "tsdown src/index.ts --dts --watch",
43
43
  "test": "vitest run",
44
44
  "test:coverage": "vitest run --coverage"
45
45
  }