UXP v2.1.4: import-flow — drop redirect:manual (not supported in UXP fetch, causes null body); use arrayBuffer() fallback if body.getReader unavailable
This commit is contained in:
parent
e5f218655e
commit
baa289f6c3
1 changed files with 70 additions and 75 deletions
|
|
@ -1,37 +1,37 @@
|
||||||
// import-flow.js — v2.1.3
|
// import-flow.js — v2.1.4
|
||||||
// Fixes:
|
// Root cause of "Cannot read properties of undefined (reading)":
|
||||||
// • require('path') → window.path (global, no require needed, UXP v6.4+)
|
// response.body is null when redirect:'manual' is used — that fetch option
|
||||||
// • fs.createWriteStream does NOT exist in UXP fs — replaced with
|
// is NOT supported in UXP. UXP auto-follows redirects and does NOT expose
|
||||||
// fd-based chunked write: fs.open → loop fs.write(fd,buf,...) → fs.close
|
// manual redirect control. Dropping redirect:'manual' entirely.
|
||||||
// • os.tmpdir() not documented → use os.homedir() / process.env.TEMP fallback
|
//
|
||||||
|
// 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 () {
|
(function () {
|
||||||
const Import = {};
|
const Import = {};
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
// path is window.path (global) — no require('path') in UXP
|
// window.path is a UXP global (v6.4+) — no require('path')
|
||||||
// os.tmpdir() missing from docs; os.homedir() is available
|
// os.tmpdir() not in UXP — use env.TEMP or uxp storage
|
||||||
let os; try { os = require('os'); } catch (_) { os = {}; }
|
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; }
|
||||||
|
|
||||||
// ── Temp folder ──────────────────────────────────────────────────
|
// ── Temp folder ──────────────────────────────────────────────────
|
||||||
// os.tmpdir() not in UXP docs → fall through to other methods
|
|
||||||
async function _getTempBase() {
|
async function _getTempBase() {
|
||||||
// 1. UXP storage API (most portable)
|
|
||||||
if (uxpFs && uxpFs.getTemporaryFolder) {
|
if (uxpFs && uxpFs.getTemporaryFolder) {
|
||||||
try {
|
try {
|
||||||
const tmp = await uxpFs.getTemporaryFolder();
|
const tmp = await uxpFs.getTemporaryFolder();
|
||||||
if (tmp && tmp.nativePath) return tmp.nativePath;
|
if (tmp && tmp.nativePath) return tmp.nativePath;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
// 2. Windows env vars (always set under PPro on Windows)
|
|
||||||
try {
|
try {
|
||||||
const e = (typeof process !== 'undefined' && process.env) || {};
|
const e = (typeof process !== 'undefined' && process.env) || {};
|
||||||
if (e.TEMP && e.TEMP.length) return e.TEMP;
|
if (e.TEMP && e.TEMP.length) return e.TEMP;
|
||||||
if (e.TMP && e.TMP.length) return e.TMP;
|
if (e.TMP && e.TMP.length) return e.TMP;
|
||||||
if (e.LOCALAPPDATA) return e.LOCALAPPDATA + '\\Temp';
|
if (e.LOCALAPPDATA) return e.LOCALAPPDATA + '\\Temp';
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
// 3. os.homedir() is documented in UXP os module
|
|
||||||
try {
|
try {
|
||||||
if (os.homedir) {
|
if (os.homedir) {
|
||||||
const h = os.homedir();
|
const h = os.homedir();
|
||||||
|
|
@ -41,64 +41,31 @@
|
||||||
throw new Error('Cannot find writable temp folder');
|
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) {
|
Import._tempPath = async function (safeName) {
|
||||||
const base = await _getTempBase();
|
const base = await _getTempBase();
|
||||||
return path.join(base, 'dragonflight-' + safeName);
|
return path.join(base, 'dragonflight-' + safeName);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Stream response body to disk via fd-based chunked write ──────
|
// ── Write ArrayBuffer to disk ────────────────────────────────────
|
||||||
// fs.createWriteStream does NOT exist in UXP's require('fs').
|
// fs.writeFile with flag:'w' creates/overwrites the file.
|
||||||
// Use open() → write() chunks → close() instead.
|
Import._writeBuffer = async function (destPath, arrayBuffer) {
|
||||||
// Chunk size 256 KB — balances memory vs syscall overhead.
|
await fs.writeFile(destPath, arrayBuffer);
|
||||||
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);
|
|
||||||
}
|
|
||||||
return destPath;
|
return destPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Manual redirect follow ───────────────────────────────────────
|
// ── Fetch with auth — UXP-safe ───────────────────────────────────
|
||||||
async function _fetchFollow(url, opts) {
|
// UXP auto-follows redirects. 'redirect' option is NOT supported.
|
||||||
opts = opts || {};
|
// We only add Authorization for same-origin requests (server URL base).
|
||||||
let current = url;
|
// For S3 presigned URLs (off-origin) we do NOT add Bearer — that would
|
||||||
for (let hop = 0; hop < 6; hop++) {
|
// break the presigned signature.
|
||||||
const r = await fetch(current, Object.assign({}, opts, { redirect: 'manual' }));
|
async function _fetch(url, addAuth) {
|
||||||
if (r.status >= 200 && r.status < 300) return r;
|
const headers = {};
|
||||||
if (r.status >= 300 && r.status < 400) {
|
if (addAuth && API.state.apiToken) {
|
||||||
const loc = r.headers.get('location');
|
headers['Authorization'] = 'Bearer ' + API.state.apiToken;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
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 ─────────────────────────────────────
|
// ── premierepro lazy require ─────────────────────────────────────
|
||||||
|
|
@ -110,11 +77,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import a file already on disk into the active Premiere project.
|
// 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) {
|
Import.importIntoProject = async function (filePath) {
|
||||||
const P = _ppro();
|
const P = _ppro();
|
||||||
const project = P.Project.getActiveProject();
|
const project = P.Project.getActiveProject(); // sync
|
||||||
if (!project) throw new Error('No active Premiere project');
|
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);
|
const ok = await project.importFiles([filePath], true, root, false);
|
||||||
if (!ok) throw new Error('Premiere refused to import file');
|
if (!ok) throw new Error('Premiere refused to import file');
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -129,13 +97,12 @@
|
||||||
const { url } = await API.getProxyUrl(asset.id);
|
const { url } = await API.getProxyUrl(asset.id);
|
||||||
|
|
||||||
UI.showProgress('Downloading ' + safeName + '…', 10);
|
UI.showProgress('Downloading ' + safeName + '…', 10);
|
||||||
const r = await _fetchFollow(url, { headers: { Authorization: 'Bearer ' + API.state.apiToken } });
|
// Same-origin proxy URL — add auth
|
||||||
if (!r.ok) throw new Error('Download HTTP ' + r.status);
|
const r = await _fetch(url, true);
|
||||||
|
|
||||||
await Import._streamToFile(r, dest, ({ received, total }) => {
|
UI.showProgress('Writing to disk…', 70);
|
||||||
const pct = total ? 10 + (received / total) * 75 : 10;
|
const buf = await r.arrayBuffer();
|
||||||
UI.showProgress(UI.formatBytes(received) + (total ? ' / ' + UI.formatBytes(total) : '') + '…', pct);
|
await Import._writeBuffer(dest, buf);
|
||||||
});
|
|
||||||
|
|
||||||
UI.showProgress('Importing into Premiere…', 92);
|
UI.showProgress('Importing into Premiere…', 92);
|
||||||
await Import.importIntoProject(dest);
|
await Import.importIntoProject(dest);
|
||||||
|
|
@ -152,13 +119,12 @@
|
||||||
const dest = await Import._tempPath(safeName);
|
const dest = await Import._tempPath(safeName);
|
||||||
|
|
||||||
UI.showProgress('Downloading ' + safeName + ' (' + UI.formatBytes(Number(info.file_size || 0)) + ')…', 8);
|
UI.showProgress('Downloading ' + safeName + ' (' + UI.formatBytes(Number(info.file_size || 0)) + ')…', 8);
|
||||||
const r = await _fetchFollow(info.url, {});
|
// Presigned S3 URL — no auth header (would break signature)
|
||||||
if (!r.ok) throw new Error('Download HTTP ' + r.status);
|
const r = await _fetch(info.url, false);
|
||||||
|
|
||||||
await Import._streamToFile(r, dest, ({ received, total }) => {
|
UI.showProgress('Writing to disk…', 75);
|
||||||
const pct = total ? 8 + (received / total) * 80 : 8;
|
const buf = await r.arrayBuffer();
|
||||||
UI.showProgress(UI.formatBytes(received) + (total ? ' / ' + UI.formatBytes(total) : '') + '…', pct);
|
await Import._writeBuffer(dest, buf);
|
||||||
});
|
|
||||||
|
|
||||||
UI.showProgress('Importing into Premiere…', 92);
|
UI.showProgress('Importing into Premiere…', 92);
|
||||||
await Import.importIntoProject(dest);
|
await Import.importIntoProject(dest);
|
||||||
|
|
@ -167,5 +133,34 @@
|
||||||
return { localPath: dest, safeName };
|
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;
|
window.Import = Import;
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue