@bsv/sdk
Version:
BSV Blockchain Software Development Kit
177 lines • 7.62 kB
JavaScript
import { AuthFetch } from '../auth/clients/AuthFetch.js';
import * as StorageUtils from './StorageUtils.js';
/**
* The StorageUploader class provides client-side methods for:
* - Uploading files with a specified retention period
* - Finding file metadata by UHRP URL
* - Listing all user uploads
* - Renewing an existing advertisement's expiry time
*/
export class StorageUploader {
authFetch;
baseURL;
/**
* Creates a new StorageUploader instance.
* @param {UploaderConfig} config - An object containing the storage server's URL and a wallet interface
*/
constructor(config) {
this.baseURL = config.storageURL;
this.authFetch = new AuthFetch(config.wallet);
}
/**
* Requests information from the server to upload a file (including presigned URL and headers).
* @private
* @param {number} fileSize - The size of the file, in bytes
* @param {number} retentionPeriod - The desired hosting time, in minutes
* @returns {Promise<{ uploadURL: string; requiredHeaders: Record<string, string>; amount?: number }>}
* @throws {Error} If the server returns a non-OK response or an error status
*/
async getUploadInfo(fileSize, retentionPeriod) {
const url = `${this.baseURL}/upload`;
const body = { fileSize, retentionPeriod };
const response = await this.authFetch.fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!response.ok) {
throw new Error(`Upload info request failed: HTTP ${response.status}`);
}
const data = await response.json();
if (data.status === 'error') {
throw new Error('Upload route returned an error.');
}
return {
uploadURL: data.uploadURL,
requiredHeaders: data.requiredHeaders,
amount: data.amount
};
}
/**
* Performs the actual file upload (HTTP PUT) to the presigned URL returned by the server.
* @private
* @param {string} uploadURL - The presigned URL where the file is to be uploaded
* @param {UploadableFile} file - The file to upload, including its raw data and MIME type
* @param {Record<string, string>} requiredHeaders - Additional headers required by the server (e.g. content-length)
* @returns {Promise<UploadFileResult>} An object indicating whether publishing was successful and the resulting UHRP URL
* @throws {Error} If the server returns a non-OK response
*/
async uploadFile(uploadURL, file, requiredHeaders) {
const body = Uint8Array.from(file.data);
const response = await fetch(uploadURL, {
method: 'PUT',
body,
headers: {
'Content-Type': file.type,
...requiredHeaders
}
});
if (!response.ok) {
throw new Error(`File upload failed: HTTP ${response.status}`);
}
const uhrpURL = await StorageUtils.getURLForFile(file.data);
return {
published: true,
uhrpURL
};
}
/**
* Publishes a file to the storage server with the specified retention period.
*
* This will:
* 1. Request an upload URL from the server.
* 2. Perform an HTTP PUT to upload the file’s raw bytes.
* 3. Return a UHRP URL referencing the file once published.
*
* @param {Object} params
* @param {UploadableFile} params.file - The file data + type
* @param {number} params.retentionPeriod - Number of minutes to host the file
* @returns {Promise<UploadFileResult>} An object with the file's UHRP URL
* @throws {Error} If the server or upload step returns a non-OK response
*/
async publishFile(params) {
const { file, retentionPeriod } = params;
const fileSize = file.data.length;
const { uploadURL, requiredHeaders } = await this.getUploadInfo(fileSize, retentionPeriod);
return await this.uploadFile(uploadURL, file, requiredHeaders);
}
/**
* Retrieves metadata for a file matching the given UHRP URL from the `/find` route.
* @param {string} uhrpUrl - The UHRP URL, e.g. "uhrp://abcd..."
* @returns {Promise<FindFileData>} An object with file name, size, MIME type, and expiry time
* @throws {Error} If the server or the route returns an error
*/
async findFile(uhrpUrl) {
const url = new URL(`${this.baseURL}/find`);
url.searchParams.set('uhrpUrl', uhrpUrl);
const response = await this.authFetch.fetch(url.toString(), {
method: 'GET'
});
if (!response.ok) {
throw new Error(`findFile request failed: HTTP ${response.status}`);
}
const data = await response.json();
if (data.status === 'error') {
const errCode = data.code ?? 'unknown-code';
const errDesc = data.description ?? 'no-description';
throw new Error(`findFile returned an error: ${errCode} - ${errDesc}`);
}
return data.data;
}
/**
* Lists all advertisements belonging to the user from the `/list` route.
* @returns {Promise<any>} The array of uploads returned by the server
* @throws {Error} If the server or the route returns an error
*/
async listUploads() {
const url = `${this.baseURL}/list`;
const response = await this.authFetch.fetch(url, {
method: 'GET'
});
if (!response.ok) {
throw new Error(`listUploads request failed: HTTP ${response.status}`);
}
const data = await response.json();
if (data.status === 'error') {
const errCode = data.code ?? 'unknown-code';
const errDesc = data.description ?? 'no-description';
throw new Error(`listUploads returned an error: ${errCode} - ${errDesc}`);
}
return data.uploads;
}
/**
* Renews the hosting time for an existing file advertisement identified by uhrpUrl.
* Calls the `/renew` route to add `additionalMinutes` to the GCS customTime
* and re-mint the advertisement token on-chain.
*
* @param {string} uhrpUrl - The UHRP URL of the file (e.g., "uhrp://abcd1234...")
* @param {number} additionalMinutes - The number of minutes to extend
* @returns {Promise<RenewFileResult>} An object with the new and previous expiry times, plus any cost
* @throws {Error} If the request fails or the server returns an error
*/
async renewFile(uhrpUrl, additionalMinutes) {
const url = `${this.baseURL}/renew`;
const body = { uhrpUrl, additionalMinutes };
const response = await this.authFetch.fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!response.ok) {
throw new Error(`renewFile request failed: HTTP ${response.status}`);
}
const data = await response.json();
if (data.status === 'error') {
const errCode = data.code ?? 'unknown-code';
const errDesc = data.description ?? 'no-description';
throw new Error(`renewFile returned an error: ${errCode} - ${errDesc}`);
}
return {
status: data.status,
prevExpiryTime: data.prevExpiryTime,
newExpiryTime: data.newExpiryTime,
amount: data.amount
};
}
}
//# sourceMappingURL=StorageUploader.js.map