Skip to content

Commit 41585bd

Browse files
authored
[Blueprints] Fallback to URL-based file name when fetching remote ZIP files (#2470)
## Motivation for the change, related issues Fixes #2464. Sometime, the URL resource fetches the remote file and yields an empty filename, which causes troubles the `installTheme` step (it can't infer the final plugin filename). This PR ensures the downloaded `File` object always has a filename. ## Implementation details Adds two filename fallbacks: 1. Content-Disposition headers 2. The fetched URL (urlencoded) ## Testing Instructions (or ideally a Blueprint) Confirm this Blueprint works in your local browser: ```json { "steps": [ { "step": "installPlugin", "pluginData": { "resource": "url", "url": "https://github-proxy.com/proxy/?repo=woocommerce/woocommerce&release=latest&asset=woocommerce.zip" } }, { "step": "installPlugin", "pluginData": { "resource": "url", "url": "https://github-proxy.com/proxy/?repo=woocommerce/wc-smooth-generator&release=latest&asset=wc-smooth-generator.zip" } } ] } ``` cc @bacoords
1 parent d3f20e3 commit 41585bd

File tree

1 file changed

+49
-1
lines changed

1 file changed

+49
-1
lines changed

packages/playground/blueprints/src/lib/resources.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,13 @@ export abstract class FetchResource extends Resource<File> {
348348
if (response.status !== 200) {
349349
throw new Error(`Could not download "${url}"`);
350350
}
351-
return new File([await response.blob()], this.name);
351+
const filename =
352+
this.name ||
353+
parseContentDisposition(
354+
response.headers.get('content-disposition') || ''
355+
) ||
356+
encodeURIComponent(url);
357+
return new File([await response.blob()], filename);
352358
} catch (e) {
353359
throw new Error(
354360
`Could not download "${url}".
@@ -411,6 +417,48 @@ export abstract class FetchResource extends Resource<File> {
411417
}
412418
}
413419

420+
/**
421+
* Parses the Content-Disposition header to extract the filename.
422+
*
423+
* @param contentDisposition The Content-Disposition header value
424+
* @returns The filename if found, null otherwise
425+
*/
426+
function parseContentDisposition(contentDisposition: string): string | null {
427+
if (!contentDisposition) {
428+
return null;
429+
}
430+
431+
// Handle both filename and filename* parameters
432+
const filenameMatch = contentDisposition.match(/filename\*?=([^;]+)/i);
433+
if (!filenameMatch) {
434+
return null;
435+
}
436+
437+
let filename = filenameMatch[1].trim();
438+
439+
// Remove surrounding quotes
440+
if (
441+
(filename.startsWith('"') && filename.endsWith('"')) ||
442+
(filename.startsWith("'") && filename.endsWith("'"))
443+
) {
444+
filename = filename.slice(1, -1);
445+
}
446+
447+
// Handle RFC 5987 encoded filenames (filename*=UTF-8''example.txt)
448+
if (filenameMatch[0].includes('filename*')) {
449+
const encodedMatch = filename.match(/^[^']*'[^']*'(.+)$/);
450+
if (encodedMatch) {
451+
try {
452+
filename = decodeURIComponent(encodedMatch[1]);
453+
} catch {
454+
// If decoding fails, use the original filename
455+
}
456+
}
457+
}
458+
459+
return filename;
460+
}
461+
414462
// eslint-disable-next-line @typescript-eslint/no-empty-function
415463
const noop = (() => {}) as any;
416464

0 commit comments

Comments
 (0)