Repository: directus/directus
File: packages/storage-driver-supabase/src/index.ts (TypeScript)

Code before:
	async exists(filepath: string) {
		try {
			await this.stat(filepath);
			return true;
		} catch {
			return false;
		}
	}

	async move(src: string, dest: string) {
		await this.bucket.move(this.fullPath(src), this.fullPath(dest));
	}

	async copy(src: string, dest: string) {
		await this.bucket.copy(this.fullPath(src), this.fullPath(dest));
	}

	async write(filepath: string, content: Readable, type?: string) {
		const { error } = await this.bucket.upload(this.fullPath(filepath), content, {
			contentType: type ?? '',
			cacheControl: '3600',
			upsert: true,
			duplex: 'half',
		});

		if (error) throw error;
	}

	async delete(filepath: string) {
		await this.bucket.remove([this.fullPath(filepath)]);
	}

	list(prefix = ''): AsyncIterable<string> {
		const fullPrefix = this.fullPath(prefix);
		return this.listGenerator(fullPrefix);
	}

	async *listGenerator(prefix: string): AsyncIterable<string> {
		const limit = 1000;
		let offset = 0;
		let itemCount = 0;

		/*
		 *	The Supabase API only returns the directories and files directly within the queried location
		 *	(called prefix in their API) matching the search query. Directories can be identified by the id being null.
		 *
		 *	Since it's unknown whether the last part of the prefix param is a directory, a file or part of a directory or file name,
		 *	the first query will be the largest common denominator (the parent directory part of the prefix)
		 *	and the results then filtered with the remaining part of the query.
		 *
		 *	This can lead to the following outcomes:

Review comment (suggestion):
Throwing our own `Error` instead of re-throwing makes the stack trace more meaningful
 ```suggestion
		if (error) {
			throw new Error(`Can't upload file "${filepath}"`, { cause: error });
		}
```



##### Re-throw Error Log
```js

err: {
      "type": "StorageApiError",
      "message": "Invalid key: ",
      "stack":
          StorageApiError: Invalid key: 
              at <anonymous> (/<path>/directus/node_modules/.pnpm/@supabase+storage-js@2.12.1/node_modules/@supabase/storage-js/src/lib/fetch.ts:33:16)
              at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
      "__isStorageError": true,
      "name": "StorageApiError",
      "status": 400,
      "statusCode": "400"
    }
```
##### Custom Error Contructor Error Log
```js
      err: {
      "type": "Error",
      "message": "Can't upload file \"1864971a-822c-428e-a107-7bff2ab79b49.jpg\": Invalid key: : Invalid key: ",
      "stack":
          Error: Can't upload file "1864971a-822c-428e-a107-7bff2ab79b49.jpg": Invalid key: 
              at DriverSupabase.write (<path>/directus/packages/storage-driver-supabase/dist/index.js:90:20)
              at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
              at async FilesService.uploadOne (<path>/directus/api/src/services/files.ts:137:5)
              at async Multipart.<anonymous> (<path>/directus/api/src/controllers/files.ts:109:23)
          caused by: StorageApiError: Invalid key: 
              at <anonymous> (<path>/directus/node_modules/.pnpm/@supabase+storage-js@2.12.1/node_modules/@supabase/storage-js/src/lib/fetch.ts:33:16)
              at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    }
```

Code after:
	async exists(filepath: string) {
		try {
			await this.stat(filepath);
			return true;
		} catch {
			return false;
		}
	}

	async move(src: string, dest: string) {
		await this.bucket.move(this.fullPath(src), this.fullPath(dest));
	}

	async copy(src: string, dest: string) {
		await this.bucket.copy(this.fullPath(src), this.fullPath(dest));
	}

	async write(filepath: string, content: Readable, type?: string) {
		const { error } = await this.bucket.upload(this.fullPath(filepath), content, {
			contentType: type ?? '',
			cacheControl: '3600',
			upsert: true,
			duplex: 'half',
		});

		if (error) {
			throw new Error(`Error uploading file "${filepath}"`, { cause: error });
		}
	}

	async delete(filepath: string) {
		await this.bucket.remove([this.fullPath(filepath)]);
	}

	list(prefix = ''): AsyncIterable<string> {
		const fullPrefix = this.fullPath(prefix);
		return this.listGenerator(fullPrefix);
	}

	async *listGenerator(prefix: string): AsyncIterable<string> {
		const limit = 1000;
		let offset = 0;
		let itemCount = 0;

		/*
		 *	The Supabase API only returns the directories and files directly within the queried location
		 *	(called prefix in their API) matching the search query. Directories can be identified by the id being null.
		 *
		 *	Since it's unknown whether the last part of the prefix param is a directory, a file or part of a directory or file name,
		 *	the first query will be the largest common denominator (the parent directory part of the prefix)
		 *	and the results then filtered with the remaining part of the query.