mirror of
https://github.com/SrIzan10/vdo.ninja.git
synced 2026-05-01 11:05:24 +00:00
243 lines
7.3 KiB
JavaScript
243 lines
7.3 KiB
JavaScript
import { waitForLegacySession } from '../legacy/session-bridge.js';
|
|
|
|
const DRIVE_CHUNK_ALIGNMENT = 256 * 1024;
|
|
const DEFAULT_DRIVE_CHUNK_SIZE = 4 * 1024 * 1024;
|
|
const DEFAULT_DROPBOX_CHUNK_SIZE = 8 * 1024 * 1024;
|
|
|
|
function createAbortError() {
|
|
const error = new Error('Aborted');
|
|
error.name = 'AbortError';
|
|
return error;
|
|
}
|
|
|
|
export class CloudUploadCoordinator {
|
|
constructor(session) {
|
|
this.session = session;
|
|
}
|
|
|
|
static async create() {
|
|
const session = await waitForLegacySession();
|
|
return new CloudUploadCoordinator(session);
|
|
}
|
|
|
|
ensureDriveClient() {
|
|
if (!this.session.gdrive && typeof window.setupGoogleDriveUploader === 'function') {
|
|
this.session.gdrive = window.setupGoogleDriveUploader();
|
|
}
|
|
return this.session.gdrive || null;
|
|
}
|
|
|
|
async ensureDropboxClient(token, options = {}) {
|
|
let opts = options;
|
|
if (typeof token === 'object' && token !== null && !Array.isArray(token)) {
|
|
opts = token;
|
|
token = undefined;
|
|
}
|
|
opts = opts || {};
|
|
const forceReauth = Boolean(opts.forceReauth);
|
|
if (typeof token === 'string' && token.trim().length) {
|
|
token = token.trim();
|
|
}
|
|
const manualTokenProvided = typeof token === 'string' && token.length > 0;
|
|
const previousClient = this.session.dbx || null;
|
|
const oauthRecord =
|
|
(this.session && this.session.dropboxOAuth) ||
|
|
(typeof session !== 'undefined' ? session.dropboxOAuth : null) ||
|
|
null;
|
|
const oauthFresh = Boolean(
|
|
oauthRecord && (!oauthRecord.expiresAt || Date.now() < oauthRecord.expiresAt),
|
|
);
|
|
if (!forceReauth && !manualTokenProvided && this.session.dbx && oauthFresh) {
|
|
return this.session.dbx;
|
|
}
|
|
if (typeof window.setupDropbox !== 'function') {
|
|
return this.session.dbx || null;
|
|
}
|
|
try {
|
|
const client = await window.setupDropbox(token, opts);
|
|
if (client) {
|
|
this.session.dbx = client;
|
|
return client;
|
|
}
|
|
} catch (error) {
|
|
if (forceReauth && previousClient && !this.session.dbx) {
|
|
this.session.dbx = previousClient;
|
|
}
|
|
throw error;
|
|
}
|
|
if (!this.session.dbx && previousClient) {
|
|
this.session.dbx = previousClient;
|
|
}
|
|
return this.session.dbx || null;
|
|
}
|
|
|
|
startDriveUpload(filename, sessionUri) {
|
|
if (typeof window.setupGoogleDriveUploader !== 'function') {
|
|
throw new Error('Google Drive uploader is not available in this build.');
|
|
}
|
|
return window.setupGoogleDriveUploader(filename, sessionUri);
|
|
}
|
|
|
|
createDriveChunkWriter(filename, sessionUri) {
|
|
const uploader = this.startDriveUpload(filename, sessionUri);
|
|
return {
|
|
addChunk: (chunk) => uploader?.addChunk?.(chunk),
|
|
finalize: () => uploader?.finalize?.(),
|
|
uploader,
|
|
};
|
|
}
|
|
|
|
createDropboxChunkWriter(filename) {
|
|
if (typeof window.streamVideoToDropbox !== 'function') {
|
|
throw new Error('Dropbox uploader is not available in this build.');
|
|
}
|
|
return window.streamVideoToDropbox(filename);
|
|
}
|
|
|
|
hasDriveAccess() {
|
|
return Boolean(this.session.gdrive && this.session.gdrive.accessToken);
|
|
}
|
|
|
|
hasDropboxAccess() {
|
|
return Boolean(this.session.dbx);
|
|
}
|
|
|
|
async uploadBlob(blob, options = {}) {
|
|
if (!blob) {
|
|
throw new Error('uploadBlob expects a Blob.');
|
|
}
|
|
const {
|
|
filename,
|
|
drive = true,
|
|
dropbox = true,
|
|
onProgress,
|
|
signal,
|
|
driveChunkSize = DEFAULT_DRIVE_CHUNK_SIZE,
|
|
dropboxChunkSize = DEFAULT_DROPBOX_CHUNK_SIZE,
|
|
} = options;
|
|
|
|
const results = {};
|
|
|
|
if (drive) {
|
|
try {
|
|
results.drive = await this.uploadBlobToDrive(blob, {
|
|
filename,
|
|
onProgress,
|
|
signal,
|
|
chunkSize: driveChunkSize,
|
|
});
|
|
} catch (error) {
|
|
results.drive = { status: 'error', service: 'drive', error };
|
|
}
|
|
} else {
|
|
results.drive = { status: 'skipped', service: 'drive', reason: 'disabled' };
|
|
}
|
|
|
|
if (dropbox) {
|
|
try {
|
|
results.dropbox = await this.uploadBlobToDropbox(blob, {
|
|
filename,
|
|
onProgress,
|
|
signal,
|
|
chunkSize: dropboxChunkSize,
|
|
});
|
|
} catch (error) {
|
|
results.dropbox = { status: 'error', service: 'dropbox', error };
|
|
}
|
|
} else {
|
|
results.dropbox = { status: 'skipped', service: 'dropbox', reason: 'disabled' };
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
async uploadBlobToDrive(blob, { filename, onProgress, signal, chunkSize = DEFAULT_DRIVE_CHUNK_SIZE } = {}) {
|
|
const client = this.ensureDriveClient();
|
|
if (!client) {
|
|
return { status: 'skipped', service: 'drive', reason: 'unavailable' };
|
|
}
|
|
if (signal?.aborted) {
|
|
throw createAbortError();
|
|
}
|
|
const name = filename || `recording-${Date.now()}.wav`;
|
|
let writer;
|
|
try {
|
|
writer = this.createDriveChunkWriter(name, this.session.gdrive?.sessionUri);
|
|
} catch (error) {
|
|
return { status: 'error', service: 'drive', error };
|
|
}
|
|
if (!writer?.addChunk) {
|
|
return { status: 'error', service: 'drive', error: new Error('Drive writer unavailable') };
|
|
}
|
|
const total = blob.size || 0;
|
|
const alignment = DRIVE_CHUNK_ALIGNMENT;
|
|
const adjustedChunkSize = Math.max(alignment, Math.floor(chunkSize / alignment) * alignment);
|
|
let uploaded = 0;
|
|
for (let offset = 0; offset < total; offset += adjustedChunkSize) {
|
|
if (signal?.aborted) {
|
|
throw createAbortError();
|
|
}
|
|
const chunk = blob.slice(offset, Math.min(total, offset + adjustedChunkSize), blob.type || 'application/octet-stream');
|
|
writer.addChunk(chunk);
|
|
uploaded += chunk.size;
|
|
if (typeof onProgress === 'function') {
|
|
onProgress({
|
|
service: 'drive',
|
|
uploaded,
|
|
total,
|
|
percentage: total ? Math.min(100, Math.round((uploaded / total) * 100)) : 0,
|
|
});
|
|
}
|
|
}
|
|
writer.addChunk(false);
|
|
if (typeof writer.finalize === 'function') {
|
|
try {
|
|
await writer.finalize();
|
|
} catch (error) {
|
|
return { status: 'error', service: 'drive', error };
|
|
}
|
|
}
|
|
return { status: 'uploaded', service: 'drive', filename: name, bytes: uploaded };
|
|
}
|
|
|
|
async uploadBlobToDropbox(blob, { filename, onProgress, signal, chunkSize = DEFAULT_DROPBOX_CHUNK_SIZE } = {}) {
|
|
const client = await this.ensureDropboxClient();
|
|
if (!client) {
|
|
return { status: 'skipped', service: 'dropbox', reason: 'unavailable' };
|
|
}
|
|
if (signal?.aborted) {
|
|
throw createAbortError();
|
|
}
|
|
let writer;
|
|
const name = filename || `recording-${Date.now()}.wav`;
|
|
try {
|
|
writer = await this.createDropboxChunkWriter(name);
|
|
} catch (error) {
|
|
return { status: 'error', service: 'dropbox', error };
|
|
}
|
|
if (typeof writer !== 'function') {
|
|
return { status: 'error', service: 'dropbox', error: new Error('Dropbox writer unavailable') };
|
|
}
|
|
const total = blob.size || 0;
|
|
let uploaded = 0;
|
|
for (let offset = 0; offset < total; offset += chunkSize) {
|
|
if (signal?.aborted) {
|
|
throw createAbortError();
|
|
}
|
|
const chunk = blob.slice(offset, Math.min(total, offset + chunkSize), blob.type || 'application/octet-stream');
|
|
await writer(chunk);
|
|
uploaded += chunk.size;
|
|
if (typeof onProgress === 'function') {
|
|
onProgress({
|
|
service: 'dropbox',
|
|
uploaded,
|
|
total,
|
|
percentage: total ? Math.min(100, Math.round((uploaded / total) * 100)) : 0,
|
|
});
|
|
}
|
|
}
|
|
await writer(false);
|
|
return { status: 'uploaded', service: 'dropbox', filename: name, bytes: uploaded };
|
|
}
|
|
}
|