diff --git a/services/premiere-plugin-uxp/src/import-flow.js b/services/premiere-plugin-uxp/src/import-flow.js index 559ba1b..544d750 100644 --- a/services/premiere-plugin-uxp/src/import-flow.js +++ b/services/premiere-plugin-uxp/src/import-flow.js @@ -1,37 +1,37 @@ -// import-flow.js — v2.1.3 -// Fixes: -// • require('path') → window.path (global, no require needed, UXP v6.4+) -// • fs.createWriteStream does NOT exist in UXP fs — replaced with -// fd-based chunked write: fs.open → loop fs.write(fd,buf,...) → fs.close -// • os.tmpdir() not documented → use os.homedir() / process.env.TEMP fallback +// import-flow.js — v2.1.4 +// Root cause of "Cannot read properties of undefined (reading)": +// response.body is null when redirect:'manual' is used — that fetch option +// is NOT supported in UXP. UXP auto-follows redirects and does NOT expose +// manual redirect control. Dropping redirect:'manual' entirely. +// +// Download strategy: +// response.arrayBuffer() → write entire buffer via fs.writeFile() +// Simpler than fd-based chunked write, works for proxy files (typically <2GB). +// Progress reporting is approximate (0% → 100% on completion). (function () { const Import = {}; const fs = require('fs'); - // path is window.path (global) — no require('path') in UXP - // os.tmpdir() missing from docs; os.homedir() is available + // window.path is a UXP global (v6.4+) — no require('path') + // os.tmpdir() not in UXP — use env.TEMP or uxp storage let os; try { os = require('os'); } catch (_) { os = {}; } let uxpFs; try { uxpFs = require('uxp').storage.localFileSystem; } catch (_) { uxpFs = null; } // ── Temp folder ────────────────────────────────────────────────── - // os.tmpdir() not in UXP docs → fall through to other methods async function _getTempBase() { - // 1. UXP storage API (most portable) if (uxpFs && uxpFs.getTemporaryFolder) { try { const tmp = await uxpFs.getTemporaryFolder(); if (tmp && tmp.nativePath) return tmp.nativePath; } catch (_) {} } - // 2. Windows env vars (always set under PPro on Windows) try { const e = (typeof process !== 'undefined' && process.env) || {}; if (e.TEMP && e.TEMP.length) return e.TEMP; if (e.TMP && e.TMP.length) return e.TMP; if (e.LOCALAPPDATA) return e.LOCALAPPDATA + '\\Temp'; } catch (_) {} - // 3. os.homedir() is documented in UXP os module try { if (os.homedir) { const h = os.homedir(); @@ -41,64 +41,31 @@ throw new Error('Cannot find writable temp folder'); } - // window.path is a documented UXP global (v6.4+) — no require needed Import._tempPath = async function (safeName) { const base = await _getTempBase(); return path.join(base, 'dragonflight-' + safeName); }; - // ── Stream response body to disk via fd-based chunked write ────── - // fs.createWriteStream does NOT exist in UXP's require('fs'). - // Use open() → write() chunks → close() instead. - // Chunk size 256 KB — balances memory vs syscall overhead. - Import._streamToFile = async function (response, destPath, onProgress) { - const total = Number(response.headers.get('content-length') || 0); - const reader = response.body.getReader(); - - // Open file for writing (create/truncate) - const fd = await fs.open(destPath, 'w'); - let received = 0; - let filePos = 0; - - try { - while (true) { - const { value, done } = await reader.read(); - if (done) break; - - // value is Uint8Array; fs.write needs ArrayBuffer - const buf = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength); - const { bytesWritten } = await fs.write(fd, buf, 0, buf.byteLength, filePos); - filePos += bytesWritten; - received += value.byteLength; - if (onProgress) onProgress({ received, total }); - } - } finally { - await fs.close(fd); - } + // ── Write ArrayBuffer to disk ──────────────────────────────────── + // fs.writeFile with flag:'w' creates/overwrites the file. + Import._writeBuffer = async function (destPath, arrayBuffer) { + await fs.writeFile(destPath, arrayBuffer); return destPath; }; - // ── Manual redirect follow ─────────────────────────────────────── - async function _fetchFollow(url, opts) { - opts = opts || {}; - let current = url; - for (let hop = 0; hop < 6; hop++) { - const r = await fetch(current, Object.assign({}, opts, { redirect: 'manual' })); - if (r.status >= 200 && r.status < 300) return r; - if (r.status >= 300 && r.status < 400) { - const loc = r.headers.get('location'); - if (!loc) throw new Error('Redirect with no Location'); - const next = /^https?:\/\//i.test(loc) ? loc : new URL(loc, current).toString(); - if (new URL(next).host !== new URL(current).host) { - opts = Object.assign({}, opts, { headers: Object.assign({}, opts.headers || {}) }); - delete opts.headers['Authorization']; - } - current = next; - continue; - } - throw new Error('HTTP ' + r.status); + // ── Fetch with auth — UXP-safe ─────────────────────────────────── + // UXP auto-follows redirects. 'redirect' option is NOT supported. + // We only add Authorization for same-origin requests (server URL base). + // For S3 presigned URLs (off-origin) we do NOT add Bearer — that would + // break the presigned signature. + async function _fetch(url, addAuth) { + const headers = {}; + if (addAuth && API.state.apiToken) { + headers['Authorization'] = 'Bearer ' + API.state.apiToken; } - throw new Error('Too many redirects'); + const r = await fetch(url, { headers }); + if (!r.ok) throw new Error('HTTP ' + r.status + ' from ' + url); + return r; } // ── premierepro lazy require ───────────────────────────────────── @@ -110,11 +77,12 @@ } // Import a file already on disk into the active Premiere project. + // project.importFiles() is async (it actually imports the file). Import.importIntoProject = async function (filePath) { const P = _ppro(); - const project = P.Project.getActiveProject(); + const project = P.Project.getActiveProject(); // sync if (!project) throw new Error('No active Premiere project'); - const root = project.getRootItem(); + const root = project.getRootItem(); // sync const ok = await project.importFiles([filePath], true, root, false); if (!ok) throw new Error('Premiere refused to import file'); return true; @@ -129,13 +97,12 @@ const { url } = await API.getProxyUrl(asset.id); UI.showProgress('Downloading ' + safeName + '…', 10); - const r = await _fetchFollow(url, { headers: { Authorization: 'Bearer ' + API.state.apiToken } }); - if (!r.ok) throw new Error('Download HTTP ' + r.status); + // Same-origin proxy URL — add auth + const r = await _fetch(url, true); - await Import._streamToFile(r, dest, ({ received, total }) => { - const pct = total ? 10 + (received / total) * 75 : 10; - UI.showProgress(UI.formatBytes(received) + (total ? ' / ' + UI.formatBytes(total) : '') + '…', pct); - }); + UI.showProgress('Writing to disk…', 70); + const buf = await r.arrayBuffer(); + await Import._writeBuffer(dest, buf); UI.showProgress('Importing into Premiere…', 92); await Import.importIntoProject(dest); @@ -152,13 +119,12 @@ const dest = await Import._tempPath(safeName); UI.showProgress('Downloading ' + safeName + ' (' + UI.formatBytes(Number(info.file_size || 0)) + ')…', 8); - const r = await _fetchFollow(info.url, {}); - if (!r.ok) throw new Error('Download HTTP ' + r.status); + // Presigned S3 URL — no auth header (would break signature) + const r = await _fetch(info.url, false); - await Import._streamToFile(r, dest, ({ received, total }) => { - const pct = total ? 8 + (received / total) * 80 : 8; - UI.showProgress(UI.formatBytes(received) + (total ? ' / ' + UI.formatBytes(total) : '') + '…', pct); - }); + UI.showProgress('Writing to disk…', 75); + const buf = await r.arrayBuffer(); + await Import._writeBuffer(dest, buf); UI.showProgress('Importing into Premiere…', 92); await Import.importIntoProject(dest); @@ -167,5 +133,34 @@ return { localPath: dest, safeName }; }; + // Expose for timeline.js batch relink (it downloads hi-res files too) + Import._streamToFile = async function (response, destPath, onProgress) { + // In UXP, response.body may be null — fall back to arrayBuffer() + if (response.body && response.body.getReader) { + const total = Number(response.headers.get('content-length') || 0); + const reader = response.body.getReader(); + const fd = await fs.open(destPath, 'w'); + let received = 0, filePos = 0; + try { + while (true) { + const { value, done } = await reader.read(); + if (done) break; + const buf = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength); + const { bytesWritten } = await fs.write(fd, buf, 0, buf.byteLength, filePos); + filePos += bytesWritten; + received += value.byteLength; + if (onProgress) onProgress({ received, total }); + } + } finally { await fs.close(fd); } + } else { + // Fallback: buffer entire response + if (onProgress) onProgress({ received: 0, total: 0 }); + const buf = await response.arrayBuffer(); + await Import._writeBuffer(destPath, buf); + if (onProgress) onProgress({ received: buf.byteLength, total: buf.byteLength }); + } + return destPath; + }; + window.Import = Import; })();