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