@flystorage/google-cloud-storage
Version:
<img src="https://raw.githubusercontent.com/duna-oss/flystorage/main/flystorage.svg" width="50px" height="50px" />
188 lines (187 loc) • 7.2 kB
JavaScript
import { ChecksumIsNotAvailable, PathPrefixer, } from '@flystorage/file-storage';
import { resolveMimeType, streamHead } from '@flystorage/stream-mime-type';
import { pipeline } from 'node:stream/promises';
import { UniformBucketLevelAccessVisibilityHandling, } from './visibility-handling.js';
export class GoogleCloudStorageAdapter {
bucket;
options;
visibilityHandling;
prefixer;
constructor(bucket, options = {}, visibilityHandling = new UniformBucketLevelAccessVisibilityHandling()) {
this.bucket = bucket;
this.options = options;
this.visibilityHandling = visibilityHandling;
this.prefixer = new PathPrefixer(options.prefix ?? '');
}
async write(path, contents, options) {
let mimeType = options.mimeType;
if (mimeType === undefined) {
[mimeType, contents] = await resolveMimeType(path, contents);
}
const writeStream = this.bucket.file(this.prefixer.prefixFilePath(path))
.createWriteStream({
contentType: mimeType,
predefinedAcl: options.visibility
? this.visibilityHandling.visibilityToPredefinedAcl(options.visibility)
: undefined,
metadata: options.cacheControl ? { cacheControl: options.cacheControl } : undefined,
});
await pipeline(contents, writeStream);
}
async read(path) {
const readStream = this.bucket.file(this.prefixer.prefixFilePath(path)).createReadStream();
// force retrieval of the head to ensure the http call is made
// this ensures the error from the HTTP call is caught at the
// abstraction level
const [_, outStream] = await streamHead(readStream, 10);
return outStream;
}
async deleteFile(path) {
await this.bucket.file(this.prefixer.prefixFilePath(path)).delete({
ignoreNotFound: true,
});
}
async createDirectory(path, options) {
await this.bucket.file(this.prefixer.prefixDirectoryPath(path)).save('');
}
async copyFile(from, to, options) {
await this.bucket.file(this.prefixer.prefixFilePath(from)).copy(this.bucket.file(this.prefixer.prefixFilePath(to)));
}
async moveFile(from, to, options) {
await this.copyFile(from, to, {});
await this.deleteFile(from);
}
async stat(path) {
const [metadata] = await this.bucket.file(this.prefixer.prefixFilePath(path)).getMetadata();
return this.mapToStatEntry(metadata);
}
async *list(path, options) {
let response;
let query = {
autoPaginate: false,
delimiter: options.deep ? undefined : '/',
includeTrailingDelimiter: options.deep ? undefined : true,
prefix: this.prefixer.prefixDirectoryPath(path),
};
while (query !== null) {
[response, query] = await this.bucket.getFiles(query);
for (const item of response) {
yield this.mapToStatEntry(item.metadata);
}
}
}
mapToStatEntry(file) {
if (file.name.endsWith('/')) {
return {
type: 'directory',
isFile: false,
isDirectory: true,
path: this.prefixer.stripDirectoryPath(file.name),
};
}
return {
type: 'file',
isFile: true,
isDirectory: false,
path: this.prefixer.stripFilePath(file.name),
lastModifiedMs: file.updated ? new Date(file.updated).getTime() : undefined,
mimeType: file.contentType,
};
}
async changeVisibility(path, visibility) {
await this.visibilityHandling.changeVisibility(this.bucket.file(this.prefixer.prefixFilePath(path)), visibility);
}
async visibility(path) {
return await this.visibilityHandling.determineVisibility(this.bucket.file(this.prefixer.prefixFilePath(path)));
}
async deleteDirectory(path) {
const prefix = this.prefixer.prefixDirectoryPath(path);
await this.bucket.deleteFiles({
prefix,
});
await this.bucket.file(prefix).delete({
ignoreNotFound: true,
});
}
async fileExists(path) {
const [exists] = await this.bucket.file(this.prefixer.prefixFilePath(path)).exists();
return exists;
}
async directoryExists(path) {
const [exists] = await this.bucket.file(this.prefixer.prefixDirectoryPath(path)).exists();
if (exists) {
return true;
}
const [response] = await this.bucket.getFiles({
autoPaginate: false,
maxResults: 1,
prefix: this.prefixer.prefixDirectoryPath(path),
});
return response.length > 0;
}
async publicUrl(path, options) {
return this.bucket.file(this.prefixer.prefixFilePath(path)).publicUrl();
}
async temporaryUrl(path, options) {
const [response] = await this.bucket.file(this.prefixer.prefixFilePath(path)).getSignedUrl({
action: 'read',
expires: options.expiresAt,
});
return response;
}
async prepareUpload(path, options) {
const headers = {};
const config = {
action: 'write',
expires: options.expiresAt,
};
const contentType = options['Content-Type'] ?? options.contentType;
if (typeof contentType === 'string') {
config.contentType = contentType;
headers['Content-Type'] = contentType;
}
const [url] = await this.bucket.file(this.prefixer.prefixFilePath(path)).getSignedUrl(config);
return {
url,
headers,
method: 'PUT',
provider: 'google-cloud-storage',
};
}
async checksum(path, options) {
const algo = options.algo ?? 'md5';
if (algo !== 'md5' && algo !== 'crc32c') {
throw ChecksumIsNotAvailable.checksumNotSupported(algo);
}
const [metadata] = await this.bucket.file(this.prefixer.prefixFilePath(path)).getMetadata();
return algo === 'crc32c' ? metadata.crc32c : metadata.md5Hash;
}
async mimeType(path, options) {
const stat = await this.stat(path);
if (stat.type !== 'file' || stat.mimeType === undefined) {
throw new Error('Unable to resolve mime-type, not available in stat entry.');
}
return stat.mimeType;
}
async lastModified(path) {
const stat = await this.stat(path);
if (stat.type !== 'file' || stat.lastModifiedMs === undefined) {
throw new Error('Unable to resolve last modified time, not available in stat entry.');
}
return stat.lastModifiedMs;
}
async fileSize(path) {
const stat = await this.stat(path);
if (stat.type !== 'file' || stat.size === undefined) {
throw new Error('Unable to resolve file size, not available in stat entry.');
}
return stat.size;
}
}
/**
* BC export
*
* @deprecated
*/
export class GoogleCloudFileStorage extends GoogleCloudStorageAdapter {
}