From 76fff5efc2650eab19cbe1d39bb03d45071cb6df Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Thu, 28 May 2026 02:22:58 -0400 Subject: [PATCH] =?UTF-8?q?UXP=20v2.1.1:=20import-flow.js=20=E2=80=94=20ex?= =?UTF-8?q?pose=20=5FtempPath/=5FstreamToFile,=20return=20{localPath,safeN?= =?UTF-8?q?ame}=20from=20proxy/hires?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../premiere-plugin-uxp/src/import-flow.js | 143 +++++++----------- 1 file changed, 57 insertions(+), 86 deletions(-) diff --git a/services/premiere-plugin-uxp/src/import-flow.js b/services/premiere-plugin-uxp/src/import-flow.js index 5b8dd01..11ae88f 100644 --- a/services/premiere-plugin-uxp/src/import-flow.js +++ b/services/premiere-plugin-uxp/src/import-flow.js @@ -1,55 +1,42 @@ -// Download asset → import into the active Premiere project via UXP's -// `premierepro` module. This is what the CEP panel could no longer do -// (csInterface.evalScript callback was lost in PPro 26). +// import-flow.js — v2.1.1 +// Download asset → write to temp → importFiles() via UXP premierepro. +// Also exposes _tempPath / _streamToFile for timeline.js batch relink. (function () { const Import = {}; - // Use UXP's Node-style fs for streaming writes (avoids buffering multi-GB - // files in memory). `os` is a stripped subset — `os.tmpdir` is not exposed - // in all UXP builds, so resolve the temp folder defensively below. const fs = require('fs'); const path = require('path'); let os; try { os = require('os'); } catch (_) { os = {}; } - let uxpFs; - try { uxpFs = require('uxp').storage.localFileSystem; } catch (_) { uxpFs = null; } + let uxpFs; try { uxpFs = require('uxp').storage.localFileSystem; } catch (_) { uxpFs = null; } - // Resolve a writable temp folder using whichever API this UXP build - // actually exposes. Returns an absolute path string suitable for both - // fs.createWriteStream and premierepro.Project.importFiles. - async function getTempBase() { - // 1. Node-style os.tmpdir (newer UXP, simplest case) + // ── Temp folder ────────────────────────────────────────────────── + async function _getTempBase() { try { if (os.tmpdir) { const t = os.tmpdir(); if (typeof t === 'string' && t.length) return t; } } catch (_) {} - // 2. UXP storage API — documented portable approach if (uxpFs && uxpFs.getTemporaryFolder) { try { const tmp = await uxpFs.getTemporaryFolder(); if (tmp && tmp.nativePath) return tmp.nativePath; } catch (_) {} } - // 3. Windows env vars (always set under PPro) try { const e = (typeof process !== 'undefined' && process.env) || {}; if (e.TEMP) return e.TEMP; if (e.TMP) return e.TMP; if (e.LOCALAPPDATA) return path.join(e.LOCALAPPDATA, 'Temp'); } catch (_) {} - // 4. Homedir + Windows-style fallback try { if (os.homedir) return path.join(os.homedir(), 'AppData', 'Local', 'Temp'); } catch (_) {} - throw new Error('Could not determine a writable temp folder on this system'); + throw new Error('Cannot find writable temp folder'); } - async function tempPath(safeName) { - const base = await getTempBase(); + Import._tempPath = async function (safeName) { + const base = await _getTempBase(); return path.join(base, 'dragonflight-' + safeName); - } + }; - // Stream the body of a fetch Response to a file on disk. - // Returns the absolute path. Reports progress via onProgress({ received, total }). - async function streamToFile(response, destPath, onProgress) { - const total = Number(response.headers.get('content-length') || 0); + // ── Stream response body to disk ───────────────────────────────── + Import._streamToFile = async function (response, destPath, onProgress) { + const total = Number(response.headers.get('content-length') || 0); const reader = response.body.getReader(); - const out = fs.createWriteStream(destPath); + const out = fs.createWriteStream(destPath); let received = 0; - // Wrap close in a Promise so we don't resolve before the OS finishes - // flushing — without this, importFiles can race the writer. const closed = new Promise((resolve, reject) => { out.on('finish', resolve); out.on('error', reject); @@ -59,7 +46,6 @@ const { value, done } = await reader.read(); if (done) break; if (!out.write(Buffer.from(value))) { - // Backpressure — wait for drain so memory stays flat. await new Promise(r => out.once('drain', r)); } received += value.byteLength; @@ -70,115 +56,100 @@ } await closed; return destPath; - } + }; - // Manual-follow fetch for redirects: UXP strips Authorization across - // origins (per Adobe security policy), so for any /api/... call that might - // 302 to S3 we follow the redirect ourselves and DROP the Bearer header - // on the next hop. - async function fetchFollow(url, opts) { + // ── Manual redirect follow ─────────────────────────────────────── + // UXP strips Authorization on cross-origin redirects. Follow manually. + async function _fetchFollow(url, opts) { opts = opts || {}; let current = url; - let auth = (opts.headers || {})['Authorization'] || null; - for (let hop = 0; hop < 5; hop++) { + for (let hop = 0; hop < 6; hop++) { const r = await fetch(current, Object.assign({}, opts, { redirect: 'manual' })); - if (r.status >= 300 && r.status < 400 && r.headers.get('location')) { - const next = r.headers.get('location'); - const absNext = /^https?:\/\//i.test(next) ? next : new URL(next, current).toString(); - const sameHost = (new URL(absNext)).host === (new URL(current)).host; - opts = Object.assign({}, opts, { - headers: Object.assign({}, opts.headers || {}, sameHost ? {} : { Authorization: undefined }), - }); - if (!sameHost) delete opts.headers.Authorization; - current = absNext; + 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(); + // Drop auth on cross-origin hop + 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; } - return r; + throw new Error('HTTP ' + r.status); } throw new Error('Too many redirects'); } - // Find the new ProjectItem after importFiles. Premiere's UXP importFiles - // returns only a boolean — we diff the rootItem.getItems() snapshot. - // (Not currently used but available for callers that need the item.) - Import.locateImported = async function (project, filename) { - try { - const root = await project.getRootItem(); - const items = await root.getItems(); - for (const it of items) { - const name = await it.getName().catch(() => null); - if (name && name === filename) return it; - } - } catch (_) {} - return null; - }; - - // Talk to Premiere. Lazy-require so the module isn't loaded until the user - // actually triggers an import (faster panel boot, clearer error if UXP host - // is missing the API). - function ppro() { - if (Import._ppro) return Import._ppro; - try { Import._ppro = require('premierepro'); } - catch (e) { throw new Error('UXP premierepro module unavailable: ' + e.message); } - return Import._ppro; + // ── premierepro lazy require ───────────────────────────────────── + function _ppro() { + if (Import._ppro_mod) return Import._ppro_mod; + try { Import._ppro_mod = require('premierepro'); } + catch (e) { throw new Error('UXP premierepro unavailable: ' + e.message); } + return Import._ppro_mod; } - // Hand a downloaded file off to Premiere. + // Import a file that is already on disk into the active Premiere project. Import.importIntoProject = async function (filePath) { - const P = ppro(); + const P = _ppro(); const project = await P.Project.getActiveProject(); - if (!project) throw new Error('No active Premiere project. Open or create a project first.'); + if (!project) throw new Error('No active Premiere project'); const root = await project.getRootItem(); - // suppressUI=true, targetBin=root, asNumberedStills=false. const ok = await project.importFiles([filePath], true, root, false); - if (!ok) throw new Error('Premiere refused to import the file.'); + if (!ok) throw new Error('Premiere refused to import file'); return true; }; - // ── Proxy import: /stream returns a same-host /video URL ────────── + // ── Proxy import ───────────────────────────────────────────────── + // Returns { localPath, safeName } for caller to record import mapping. Import.proxy = async function (asset) { const safeName = UI.sanitizeFilename((asset.display_name || asset.filename || asset.id) + '.mp4'); - const dest = await tempPath(safeName); + const dest = await Import._tempPath(safeName); UI.showProgress('Resolving proxy URL…', 4); const { url } = await API.getProxyUrl(asset.id); UI.showProgress('Downloading ' + safeName + '…', 10); - const r = await fetchFollow(url, { headers: { Authorization: 'Bearer ' + API.state.apiToken } }); + const r = await _fetchFollow(url, { headers: { Authorization: 'Bearer ' + API.state.apiToken } }); if (!r.ok) throw new Error('Download HTTP ' + r.status); - await streamToFile(r, dest, ({ received, total }) => { + await Import._streamToFile(r, dest, ({ received, total }) => { const pct = total ? 10 + (received / total) * 75 : 10; - UI.showProgress('Downloading ' + UI.formatBytes(received) + (total ? ' / ' + UI.formatBytes(total) : '') + '…', pct); + UI.showProgress(UI.formatBytes(received) + (total ? ' / ' + UI.formatBytes(total) : '') + '…', pct); }); UI.showProgress('Importing into Premiere…', 92); await Import.importIntoProject(dest); UI.hideProgress(); UI.toast('Imported: ' + safeName, 'ok'); + return { localPath: dest, safeName }; }; - // ── Hi-Res import: /hires returns a presigned S3 URL on broadcastmgmt.cloud + // ── Hi-Res import ──────────────────────────────────────────────── + // Returns { localPath, safeName }. Import.hires = async function (asset) { UI.showProgress('Resolving hi-res URL…', 4); - const info = await API.getHiresInfo(asset.id); + const info = await API.getHiresInfo(asset.id); const safeName = UI.sanitizeFilename(info.filename || (asset.display_name || asset.id) + '.' + (info.ext || 'mxf')); - const dest = await tempPath(safeName); + const dest = await Import._tempPath(safeName); UI.showProgress('Downloading ' + safeName + ' (' + UI.formatBytes(Number(info.file_size || 0)) + ')…', 8); - // info.url is presigned S3 — DO NOT add Bearer (would break signature). - const r = await fetchFollow(info.url, {}); + // Presigned S3 URL — no Bearer needed + const r = await _fetchFollow(info.url, {}); if (!r.ok) throw new Error('Download HTTP ' + r.status); - await streamToFile(r, dest, ({ received, total }) => { + await Import._streamToFile(r, dest, ({ received, total }) => { const pct = total ? 8 + (received / total) * 80 : 8; - UI.showProgress('Downloading ' + UI.formatBytes(received) + (total ? ' / ' + UI.formatBytes(total) : '') + '…', pct); + UI.showProgress(UI.formatBytes(received) + (total ? ' / ' + UI.formatBytes(total) : '') + '…', pct); }); UI.showProgress('Importing into Premiere…', 92); await Import.importIntoProject(dest); UI.hideProgress(); UI.toast('Hi-res imported: ' + safeName, 'ok'); + return { localPath: dest, safeName }; }; window.Import = Import;