Add initial PsiTransfer Filelink provider scaffold
This commit is contained in:
parent
a87d020ca2
commit
953cd501a7
16 changed files with 6283 additions and 1 deletions
30
src/background.js
Normal file
30
src/background.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { AccountStore } from "./cloudfile/account-store.js";
|
||||
import { CloudFileProvider } from "./cloudfile/provider.js";
|
||||
import { error as logError, info } from "./util/log.js";
|
||||
|
||||
const accountStore = new AccountStore();
|
||||
const provider = new CloudFileProvider({ accountStore });
|
||||
|
||||
async function bootstrap() {
|
||||
browser.cloudFile.onAccountAdded.addListener(account => provider.onAccountAdded(account));
|
||||
browser.cloudFile.onAccountDeleted.addListener(accountId => provider.onAccountDeleted(accountId));
|
||||
browser.cloudFile.onFileUpload.addListener((account, fileInfo, tab, relatedFileInfo) => {
|
||||
return provider.onFileUpload(account, fileInfo, tab, relatedFileInfo);
|
||||
});
|
||||
browser.cloudFile.onFileUploadAbort.addListener((account, fileId, tab) => {
|
||||
return provider.onFileUploadAbort(account, fileId, tab);
|
||||
});
|
||||
browser.cloudFile.onFileDeleted.addListener((account, fileId, tab) => {
|
||||
return provider.onFileDeleted(account, fileId, tab);
|
||||
});
|
||||
browser.cloudFile.onFileRename.addListener((account, fileId, newName, tab) => {
|
||||
return provider.onFileRename(account, fileId, newName, tab);
|
||||
});
|
||||
|
||||
await provider.initialize();
|
||||
info("PsiTransfer Filelink scaffold initialized.");
|
||||
}
|
||||
|
||||
bootstrap().catch(caughtError => {
|
||||
logError("Failed to bootstrap background service worker", caughtError);
|
||||
});
|
||||
70
src/cloudfile/account-store.js
Normal file
70
src/cloudfile/account-store.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
const ACCOUNT_PREFIX = "account:";
|
||||
const FILE_PREFIX = "uploaded-file:";
|
||||
|
||||
export class AccountStore {
|
||||
constructor(storageArea = globalThis.browser?.storage?.local) {
|
||||
this.storageArea = storageArea;
|
||||
}
|
||||
|
||||
async get(accountId) {
|
||||
const key = this.#accountKey(accountId);
|
||||
const result = await this.storageArea.get(key);
|
||||
return result[key] ?? null;
|
||||
}
|
||||
|
||||
async set(accountId, config) {
|
||||
const key = this.#accountKey(accountId);
|
||||
await this.storageArea.set({
|
||||
[key]: {
|
||||
...config,
|
||||
accountId,
|
||||
updatedAt: Date.now()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async remove(accountId) {
|
||||
const keys = await this.storageArea.get(null);
|
||||
const removeKeys = Object.keys(keys).filter(key => {
|
||||
return key === this.#accountKey(accountId) || key.startsWith(`${this.#filePrefix(accountId)}:`);
|
||||
});
|
||||
|
||||
if (removeKeys.length) {
|
||||
await this.storageArea.remove(removeKeys);
|
||||
}
|
||||
}
|
||||
|
||||
async getUploadedFile(accountId, fileId) {
|
||||
const key = this.#fileKey(accountId, fileId);
|
||||
const result = await this.storageArea.get(key);
|
||||
return result[key] ?? null;
|
||||
}
|
||||
|
||||
async setUploadedFile(accountId, fileId, remoteRef) {
|
||||
const key = this.#fileKey(accountId, fileId);
|
||||
await this.storageArea.set({
|
||||
[key]: {
|
||||
...remoteRef,
|
||||
accountId,
|
||||
fileId,
|
||||
updatedAt: Date.now()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async removeUploadedFile(accountId, fileId) {
|
||||
await this.storageArea.remove(this.#fileKey(accountId, fileId));
|
||||
}
|
||||
|
||||
#accountKey(accountId) {
|
||||
return `${ACCOUNT_PREFIX}${accountId}`;
|
||||
}
|
||||
|
||||
#filePrefix(accountId) {
|
||||
return `${FILE_PREFIX}${accountId}`;
|
||||
}
|
||||
|
||||
#fileKey(accountId, fileId) {
|
||||
return `${this.#filePrefix(accountId)}:${fileId}`;
|
||||
}
|
||||
}
|
||||
223
src/cloudfile/provider.js
Normal file
223
src/cloudfile/provider.js
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
import { PsiTransferClient } from "../psitransfer/client.js";
|
||||
import { debug, error as logError, info, warn } from "../util/log.js";
|
||||
|
||||
/**
|
||||
* @typedef {import("../psitransfer/types.js").PsiTransferAccountConfig} PsiTransferAccountConfig
|
||||
*/
|
||||
|
||||
export class CloudFileProvider {
|
||||
constructor({ accountStore, cloudFileApi = globalThis.browser?.cloudFile }) {
|
||||
this.accountStore = accountStore;
|
||||
this.cloudFileApi = cloudFileApi;
|
||||
this.abortedUploads = new Set();
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
const accounts = await this.cloudFileApi.getAllAccounts();
|
||||
await Promise.all(accounts.map(account => this.ensureAccount(account)));
|
||||
}
|
||||
|
||||
async ensureAccount(account) {
|
||||
const config = await this.accountStore.get(account.id);
|
||||
const isConfigured = Boolean(config?.baseUrl);
|
||||
|
||||
await this.cloudFileApi.updateAccount(account.id, {
|
||||
configured: isConfigured,
|
||||
managementUrl: this.managementUrlFor(account.id),
|
||||
name: config?.displayName || account.name || "PsiTransfer",
|
||||
uploadSizeLimit: -1,
|
||||
spaceRemaining: -1,
|
||||
spaceUsed: -1
|
||||
});
|
||||
|
||||
debug("Ensured account state", account.id, { isConfigured });
|
||||
}
|
||||
|
||||
async onAccountAdded(account) {
|
||||
info("CloudFile account added", account.id);
|
||||
|
||||
const existing = await this.accountStore.get(account.id);
|
||||
if (!existing) {
|
||||
await this.accountStore.set(account.id, {
|
||||
baseUrl: "",
|
||||
uploadAppPath: "/",
|
||||
uploadPassword: "",
|
||||
defaultRetention: "604800",
|
||||
customSid: ""
|
||||
});
|
||||
}
|
||||
|
||||
await this.ensureAccount(account);
|
||||
}
|
||||
|
||||
async onAccountDeleted(accountId) {
|
||||
info("CloudFile account deleted", accountId);
|
||||
await this.accountStore.remove(accountId);
|
||||
}
|
||||
|
||||
async onFileUpload(account, fileInfo, tab, relatedFileInfo) {
|
||||
debug("Upload requested", {
|
||||
accountId: account.id,
|
||||
fileId: fileInfo.id,
|
||||
relatedFileInfo
|
||||
});
|
||||
|
||||
if (this.#consumeAbort(account.id, fileInfo.id)) {
|
||||
return { aborted: true };
|
||||
}
|
||||
|
||||
const config = await this.accountStore.get(account.id);
|
||||
if (!config?.baseUrl) {
|
||||
return {
|
||||
error:
|
||||
"PsiTransfer account is not configured. Open the provider settings and set the base PsiTransfer URL."
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new PsiTransferClient(config);
|
||||
const uploadResult = await client.uploadFile(fileInfo.data, {
|
||||
uploadId: this.#uploadKey(account.id, fileInfo.id),
|
||||
customSid: config.customSid || undefined,
|
||||
retention: config.defaultRetention || undefined,
|
||||
uploadPassword: config.uploadPassword || undefined,
|
||||
relatedUrl: relatedFileInfo?.url
|
||||
});
|
||||
|
||||
const url = client.resolveShareUrl(uploadResult);
|
||||
const remoteRef = {
|
||||
fileId: String(fileInfo.id),
|
||||
sid: uploadResult.sid,
|
||||
key: uploadResult.key,
|
||||
url,
|
||||
retention: uploadResult.retention || config.defaultRetention,
|
||||
downloadPasswordProtected: false,
|
||||
deleteSupported: false,
|
||||
renameSupported: false
|
||||
};
|
||||
|
||||
await this.accountStore.setUploadedFile(account.id, fileInfo.id, remoteRef);
|
||||
|
||||
return {
|
||||
url,
|
||||
templateInfo: this.#buildTemplateInfo(account, remoteRef)
|
||||
};
|
||||
} catch (caughtError) {
|
||||
if (this.#consumeAbort(account.id, fileInfo.id)) {
|
||||
return { aborted: true };
|
||||
}
|
||||
|
||||
logError("Upload failed", caughtError);
|
||||
return {
|
||||
error: caughtError instanceof Error ? caughtError.message : String(caughtError)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async onFileUploadAbort(account, fileId) {
|
||||
warn("Upload abort requested", { accountId: account.id, fileId });
|
||||
this.abortedUploads.add(this.#uploadKey(account.id, fileId));
|
||||
|
||||
const config = await this.accountStore.get(account.id);
|
||||
if (!config?.baseUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new PsiTransferClient(config);
|
||||
await client.abortUpload(this.#uploadKey(account.id, fileId));
|
||||
} catch (caughtError) {
|
||||
debug("Abort not fully implemented yet", caughtError);
|
||||
}
|
||||
}
|
||||
|
||||
async onFileDeleted(account, fileId) {
|
||||
warn("Delete requested", { accountId: account.id, fileId });
|
||||
|
||||
const config = await this.accountStore.get(account.id);
|
||||
const remoteRef = await this.accountStore.getUploadedFile(account.id, fileId);
|
||||
|
||||
if (!config?.baseUrl || !remoteRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new PsiTransferClient(config);
|
||||
await client.deleteUpload(remoteRef);
|
||||
await this.accountStore.removeUploadedFile(account.id, fileId);
|
||||
} catch (caughtError) {
|
||||
debug("Delete not implemented yet", caughtError);
|
||||
}
|
||||
}
|
||||
|
||||
async onFileRename(account, fileId, newName) {
|
||||
warn("Rename requested", { accountId: account.id, fileId, newName });
|
||||
|
||||
const config = await this.accountStore.get(account.id);
|
||||
const remoteRef = await this.accountStore.getUploadedFile(account.id, fileId);
|
||||
if (!config?.baseUrl || !remoteRef) {
|
||||
return {
|
||||
error: "No stored PsiTransfer upload record was found for this Thunderbird file."
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new PsiTransferClient(config);
|
||||
const result = await client.renameUpload(remoteRef, newName);
|
||||
const url = client.resolveShareUrl(result || remoteRef);
|
||||
|
||||
await this.accountStore.setUploadedFile(account.id, fileId, {
|
||||
...remoteRef,
|
||||
...result,
|
||||
url
|
||||
});
|
||||
|
||||
return { url };
|
||||
} catch (caughtError) {
|
||||
return {
|
||||
error: caughtError instanceof Error ? caughtError.message : String(caughtError)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
managementUrlFor(accountId) {
|
||||
return globalThis.browser.runtime.getURL(`ui/account.html?accountId=${encodeURIComponent(accountId)}`);
|
||||
}
|
||||
|
||||
#buildTemplateInfo(account, remoteRef) {
|
||||
const templateInfo = {
|
||||
service_name: account.name || "PsiTransfer",
|
||||
service_url: remoteRef.url
|
||||
};
|
||||
|
||||
if (remoteRef.downloadPasswordProtected) {
|
||||
templateInfo.download_password_protected = true;
|
||||
}
|
||||
|
||||
const retentionSeconds = Number(remoteRef.retention);
|
||||
if (Number.isFinite(retentionSeconds) && retentionSeconds > 0) {
|
||||
templateInfo.download_expiry_date = {
|
||||
timestamp: Date.now() + retentionSeconds * 1000
|
||||
};
|
||||
}
|
||||
|
||||
if (remoteRef.retention === "one-time") {
|
||||
templateInfo.download_limit = 1;
|
||||
}
|
||||
|
||||
return templateInfo;
|
||||
}
|
||||
|
||||
#uploadKey(accountId, fileId) {
|
||||
return `${accountId}:${fileId}`;
|
||||
}
|
||||
|
||||
#consumeAbort(accountId, fileId) {
|
||||
const key = this.#uploadKey(accountId, fileId);
|
||||
const aborted = this.abortedUploads.has(key);
|
||||
if (aborted) {
|
||||
this.abortedUploads.delete(key);
|
||||
}
|
||||
return aborted;
|
||||
}
|
||||
}
|
||||
274
src/psitransfer/client.js
Normal file
274
src/psitransfer/client.js
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
import "../../vendor/tus.js";
|
||||
import { PSI_TRANSFER_NOT_IMPLEMENTED } from "./types.js";
|
||||
|
||||
const tus = globalThis.tus;
|
||||
const activeUploads = new Map();
|
||||
const DEFAULT_CHUNK_SIZE = 5_000_000;
|
||||
|
||||
function normalizeBaseUrl(rawUrl) {
|
||||
const url = new URL(rawUrl);
|
||||
if (!url.pathname.endsWith("/")) {
|
||||
url.pathname += "/";
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function randomSid() {
|
||||
const bytes = new Uint8Array(6);
|
||||
crypto.getRandomValues(bytes);
|
||||
return Array.from(bytes, value => value.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
function normalizeUploadAppPath(rawPath) {
|
||||
const value = String(rawPath || "/").trim();
|
||||
if (!value || value === "/") {
|
||||
return "/";
|
||||
}
|
||||
if (/^[a-z]+:\/\//i.test(value)) {
|
||||
return normalizeBaseUrl(value).toString();
|
||||
}
|
||||
return `${value.replace(/^\/+/, "").replace(/\/+$/, "")}/`;
|
||||
}
|
||||
|
||||
function resolveUploadAppUrl(baseUrl, rawPath) {
|
||||
const normalizedPath = normalizeUploadAppPath(rawPath);
|
||||
if (/^[a-z]+:\/\//i.test(normalizedPath)) {
|
||||
return normalizedPath;
|
||||
}
|
||||
return normalizedPath === "/" ? normalizeBaseUrl(baseUrl).toString() : new URL(normalizedPath, normalizeBaseUrl(baseUrl)).toString();
|
||||
}
|
||||
|
||||
function absolutizeUrl(baseUrl, candidate) {
|
||||
if (!candidate) {
|
||||
return null;
|
||||
}
|
||||
return new URL(candidate, baseUrl).toString();
|
||||
}
|
||||
|
||||
function parseUploadLocation(uploadUrl) {
|
||||
if (!uploadUrl) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const pathname = new URL(uploadUrl).pathname.replace(/\/+$/, "");
|
||||
const uploadId = decodeURIComponent(pathname.substring(pathname.lastIndexOf("/") + 1));
|
||||
|
||||
if (!uploadId.includes("++")) {
|
||||
return { uploadId };
|
||||
}
|
||||
|
||||
const [sid, key] = uploadId.split("++");
|
||||
return { sid, key, uploadId };
|
||||
}
|
||||
|
||||
async function readErrorBody(response) {
|
||||
try {
|
||||
return await response.text();
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function describeTusError(caughtError) {
|
||||
const response = caughtError?.originalResponse;
|
||||
const status = response?.getStatus?.();
|
||||
const body = response?.getBody?.();
|
||||
|
||||
if (body) {
|
||||
try {
|
||||
const parsed = JSON.parse(body);
|
||||
if (parsed?.message) {
|
||||
return `PsiTransfer upload failed (${status || "unknown"}): ${parsed.message}`;
|
||||
}
|
||||
} catch {
|
||||
return `PsiTransfer upload failed (${status || "unknown"}): ${body}`;
|
||||
}
|
||||
return `PsiTransfer upload failed (${status || "unknown"}): ${body}`;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
return `PsiTransfer upload failed (${status}).`;
|
||||
}
|
||||
|
||||
return caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
}
|
||||
|
||||
export class PsiTransferClient {
|
||||
constructor(config) {
|
||||
if (!config?.baseUrl) {
|
||||
throw new Error("PsiTransferClient requires a baseUrl.");
|
||||
}
|
||||
|
||||
this.config = {
|
||||
...config,
|
||||
baseUrl: normalizeBaseUrl(config.baseUrl).toString(),
|
||||
uploadAppPath: config.uploadAppPath || "/"
|
||||
};
|
||||
|
||||
if (!tus?.Upload) {
|
||||
throw new Error("Vendored tus client did not load. Verify vendor/tus.js is packaged and accessible to the background service worker.");
|
||||
}
|
||||
}
|
||||
|
||||
async fetchServerConfig(options = {}) {
|
||||
const endpoint = new URL("config.json", this.config.baseUrl);
|
||||
const response = await fetch(endpoint, {
|
||||
headers: this.#buildHeaders(options.uploadPassword)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await readErrorBody(response);
|
||||
throw new Error(
|
||||
`PsiTransfer config probe failed (${response.status}). ${body || "Check the base URL and upload password."}`.trim()
|
||||
);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async buildUploadPlan(file, options = {}) {
|
||||
const baseUrl = normalizeBaseUrl(this.config.baseUrl);
|
||||
const uploadAppUrl = resolveUploadAppUrl(baseUrl, options.uploadAppPath || this.config.uploadAppPath);
|
||||
const serverConfig = await this.fetchServerConfig(options);
|
||||
const sid = options.customSid || this.config.customSid || randomSid();
|
||||
const retention = options.retention || this.config.defaultRetention || serverConfig.defaultRetention || "604800";
|
||||
const uploadEndpoint = new URL("files", uploadAppUrl).toString();
|
||||
const lockEndpoint = new URL(`files/${encodeURIComponent(sid)}?lock=yes`, uploadAppUrl).toString();
|
||||
const shareUrl = new URL(encodeURIComponent(sid), baseUrl).toString();
|
||||
const supportedRetentions = Object.keys(serverConfig?.retentions || {});
|
||||
|
||||
if (serverConfig?.requireBucketPassword && !options.bucketPassword) {
|
||||
throw new Error("This PsiTransfer server requires a bucket password, but the Thunderbird provider does not expose bucket-password configuration yet.");
|
||||
}
|
||||
if (serverConfig?.maxFileSize && file.size > Number(serverConfig.maxFileSize)) {
|
||||
throw new Error(`File exceeds the PsiTransfer max upload size of ${serverConfig.maxFileSize} bytes.`);
|
||||
}
|
||||
if (supportedRetentions.length && !supportedRetentions.includes(retention)) {
|
||||
throw new Error(`Retention "${retention}" is not accepted by this PsiTransfer server. Supported values: ${supportedRetentions.join(", ")}.`);
|
||||
}
|
||||
|
||||
return {
|
||||
sid,
|
||||
shareUrl,
|
||||
uploadAppUrl,
|
||||
uploadEndpoint,
|
||||
lockEndpoint,
|
||||
headers: this.#buildHeaders(options.uploadPassword),
|
||||
retention,
|
||||
metadata: {
|
||||
sid,
|
||||
retention,
|
||||
name: file.name,
|
||||
type: file.type || "application/octet-stream",
|
||||
...(options.bucketPassword ? { password: options.bucketPassword } : {})
|
||||
},
|
||||
sourceNote:
|
||||
"Derived from /tmp/psitransfer/lib/endpoints.js:133-158,418-552, /tmp/psitransfer/lib/tusboy/handlers/post.js, and /tmp/psitransfer/app/src/Upload/store/upload.js:127-246. PsiTransfer expects tus metadata including name, sid, retention, and optional password, then a bucket lock PATCH once uploads complete."
|
||||
};
|
||||
}
|
||||
|
||||
async uploadFile(file, options = {}) {
|
||||
const plan = await this.buildUploadPlan(file, options);
|
||||
const uploadId = String(options.uploadId || `${plan.sid}:${file.name}:${Date.now()}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let createdUploadUrl = null;
|
||||
|
||||
const cleanup = () => {
|
||||
activeUploads.delete(uploadId);
|
||||
};
|
||||
|
||||
const upload = new tus.Upload(file, {
|
||||
endpoint: plan.uploadEndpoint,
|
||||
headers: plan.headers,
|
||||
metadata: plan.metadata,
|
||||
chunkSize: DEFAULT_CHUNK_SIZE,
|
||||
parallelUploads: 1,
|
||||
storeFingerprintForResuming: false,
|
||||
retryDelays: [0, 1000, 3000, 5000],
|
||||
onAfterResponse(req, res) {
|
||||
if (req.getMethod() === "POST" && res.getStatus() === 201) {
|
||||
createdUploadUrl = absolutizeUrl(plan.uploadAppUrl, res.getHeader("location"));
|
||||
}
|
||||
},
|
||||
onError(caughtError) {
|
||||
cleanup();
|
||||
reject(new Error(describeTusError(caughtError)));
|
||||
},
|
||||
onSuccess: async () => {
|
||||
const finalUploadUrl = upload.url || createdUploadUrl;
|
||||
const uploadInfo = parseUploadLocation(finalUploadUrl);
|
||||
|
||||
try {
|
||||
await this.#lockBucket(plan.lockEndpoint, plan.headers);
|
||||
cleanup();
|
||||
resolve({
|
||||
sid: uploadInfo.sid || plan.sid,
|
||||
key: uploadInfo.key,
|
||||
uploadId: uploadInfo.uploadId,
|
||||
uploadUrl: finalUploadUrl,
|
||||
retention: plan.retention,
|
||||
url: plan.shareUrl
|
||||
});
|
||||
} catch (caughtError) {
|
||||
cleanup();
|
||||
reject(caughtError);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
activeUploads.set(uploadId, upload);
|
||||
upload.start();
|
||||
});
|
||||
}
|
||||
|
||||
async abortUpload(uploadId) {
|
||||
const upload = activeUploads.get(uploadId);
|
||||
if (!upload) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await Promise.resolve(upload.abort(true));
|
||||
activeUploads.delete(uploadId);
|
||||
return true;
|
||||
}
|
||||
|
||||
async deleteUpload(remoteRef) {
|
||||
throw new Error(
|
||||
`${PSI_TRANSFER_NOT_IMPLEMENTED} Delete support is not wired because the PsiTransfer source inspected so far does not establish a stable public delete endpoint for previously uploaded files. Remote ref: ${JSON.stringify(remoteRef)}`
|
||||
);
|
||||
}
|
||||
|
||||
async renameUpload(remoteRef, newName) {
|
||||
throw new Error(
|
||||
`${PSI_TRANSFER_NOT_IMPLEMENTED} Rename support is not wired because the PsiTransfer source inspected so far does not establish a stable public rename endpoint. Requested name: ${newName}; remote ref: ${JSON.stringify(remoteRef)}`
|
||||
);
|
||||
}
|
||||
|
||||
resolveShareUrl(result) {
|
||||
if (result?.url) {
|
||||
return result.url;
|
||||
}
|
||||
if (result?.sid) {
|
||||
return new URL(encodeURIComponent(result.sid), normalizeBaseUrl(this.config.baseUrl)).toString();
|
||||
}
|
||||
throw new Error("Cannot resolve PsiTransfer share URL without a url or sid.");
|
||||
}
|
||||
|
||||
#buildHeaders(uploadPassword) {
|
||||
const password = uploadPassword ?? this.config.uploadPassword;
|
||||
return password ? { "x-passwd": password } : {};
|
||||
}
|
||||
|
||||
async #lockBucket(lockEndpoint, headers) {
|
||||
const response = await fetch(lockEndpoint, {
|
||||
method: "PATCH",
|
||||
headers
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await readErrorBody(response);
|
||||
throw new Error(`PsiTransfer bucket lock failed (${response.status}). ${body}`.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/psitransfer/types.js
Normal file
48
src/psitransfer/types.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* @typedef {Object} PsiTransferAccountConfig
|
||||
* @property {string} baseUrl
|
||||
* @property {string=} uploadAppPath
|
||||
* @property {string=} uploadPassword
|
||||
* @property {string=} defaultRetention
|
||||
* @property {string=} customSid
|
||||
*/
|
||||
|
||||
/**
|
||||
* Source-informed upload contract derived from /tmp/psitransfer.
|
||||
*
|
||||
* @typedef {Object} PsiTransferUploadRequest
|
||||
* @property {File} file
|
||||
* @property {string} fileId
|
||||
* @property {string} sid
|
||||
* @property {string} retention
|
||||
* @property {string=} bucketPassword
|
||||
* @property {string=} uploadPassword
|
||||
* @property {string=} relatedUrl
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PsiTransferUploadPlan
|
||||
* @property {string} sid
|
||||
* @property {string} shareUrl
|
||||
* @property {string} uploadAppUrl
|
||||
* @property {string} uploadEndpoint
|
||||
* @property {string} lockEndpoint
|
||||
* @property {Object<string, string>} headers
|
||||
* @property {Object<string, string>} metadata
|
||||
* @property {string} retention
|
||||
* @property {string} sourceNote
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PsiTransferRemoteFileRef
|
||||
* @property {string} fileId
|
||||
* @property {string} sid
|
||||
* @property {string=} key
|
||||
* @property {string} url
|
||||
* @property {string=} retention
|
||||
* @property {boolean=} downloadPasswordProtected
|
||||
* @property {boolean=} deleteSupported
|
||||
* @property {boolean=} renameSupported
|
||||
*/
|
||||
|
||||
export const PSI_TRANSFER_NOT_IMPLEMENTED = "PsiTransfer integration is not implemented yet.";
|
||||
21
src/util/log.js
Normal file
21
src/util/log.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
const PREFIX = "[psitransfer-filelink]";
|
||||
|
||||
function format(args) {
|
||||
return [PREFIX, ...args];
|
||||
}
|
||||
|
||||
export function debug(...args) {
|
||||
console.debug(...format(args));
|
||||
}
|
||||
|
||||
export function info(...args) {
|
||||
console.info(...format(args));
|
||||
}
|
||||
|
||||
export function warn(...args) {
|
||||
console.warn(...format(args));
|
||||
}
|
||||
|
||||
export function error(...args) {
|
||||
console.error(...format(args));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue