Skip to content

Commit ca8222c

Browse files
Merge pull request #2616 from Chia-Network/checkpoint/main_from_release_2.5.2_3b65c4febd8630753237d867c8fee62db2a759bd
checkpoint: into main from release/2.5.2 @ 3b65c4f
2 parents c528927 + 87bd4f7 commit ca8222c

10 files changed

+269
-147
lines changed

package-lock.json

+22
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/gui/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
"react-virtuoso": "4.5.0",
9292
"redux": "4.2.1",
9393
"regenerator-runtime": "0.14.1",
94+
"sanitize-filename": "1.6.3",
9495
"seedrandom": "3.0.5",
9596
"stacktrace-js": "2.0.2",
9697
"stream-browserify": "3.0.0",

packages/gui/src/components/nfts/MultipleDownloadDialog.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default function MultipleDownloadDialog(props: MultipleDownloadDialogProp
1717
const [progressObject, setProgressObject] = useState<any>({
1818
progress: 0,
1919
url: '',
20-
i: 1,
20+
index: 0,
2121
total: 1,
2222
});
2323
const [responseObject, setResponseObject] = useState<any>({});
@@ -31,11 +31,11 @@ export default function MultipleDownloadDialog(props: MultipleDownloadDialogProp
3131
setResponseObject(obj);
3232
setDownloadDone(true);
3333
};
34-
ipcRenderer.on('downloadProgress', downloadProgressFn);
34+
ipcRenderer.on('multipleDownloadProgress', downloadProgressFn);
3535
ipcRenderer.on('multipleDownloadDone', downloadDoneFn);
3636

3737
return () => {
38-
ipcRenderer.off('downloadProgress', downloadProgressFn);
38+
ipcRenderer.off('multipleDownloadProgress', downloadProgressFn);
3939
ipcRenderer.off('multipleDownloadDone', downloadDoneFn);
4040
};
4141
}, []);
@@ -75,7 +75,7 @@ export default function MultipleDownloadDialog(props: MultipleDownloadDialogProp
7575
}
7676
return (
7777
<Trans>
78-
Downloading files {progressObject.i + 1}/{progressObject.total}
78+
Downloading files {progressObject.index + 1}/{progressObject.total}
7979
</Trans>
8080
);
8181
}
@@ -102,7 +102,7 @@ export default function MultipleDownloadDialog(props: MultipleDownloadDialogProp
102102
<Box>{progressObject.url}</Box>
103103
<Box
104104
sx={{
105-
width: `${progressObject.progress * 100}%`,
105+
width: `${progressObject.progress}%`,
106106
height: '12px',
107107
background: `${theme.palette.primary.main}`,
108108
borderRadius: '3px',

packages/gui/src/components/nfts/NFTContextualActions.tsx

+54-30
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable no-bitwise -- enable bitwise operators for this file */
22

3-
import type { NFTInfo } from '@chia-network/api';
3+
import { toBech32m, type NFTInfo } from '@chia-network/api';
44
import { useSetNFTStatusMutation, useLocalStorage } from '@chia-network/api-react';
55
import { AlertDialog, DropdownActions, MenuItem, useOpenDialog, isValidURL } from '@chia-network/core';
66
import {
@@ -32,6 +32,7 @@ import useOpenUnsafeLink from '../../hooks/useOpenUnsafeLink';
3232
import useViewNFTOnExplorer, { NFTExplorer } from '../../hooks/useViewNFTOnExplorer';
3333
import NFTSelection from '../../types/NFTSelection';
3434
import download from '../../util/download';
35+
import getFileExtension from '../../util/getFileExtension';
3536
import removeHexPrefix from '../../util/removeHexPrefix';
3637

3738
import MultipleDownloadDialog from './MultipleDownloadDialog';
@@ -392,53 +393,76 @@ type NFTDownloadContextualActionProps = NFTContextualActionProps;
392393

393394
function NFTDownloadContextualAction(props: NFTDownloadContextualActionProps) {
394395
const { selection } = props;
395-
const selectedNft: NFTInfo | undefined = selection?.items[0];
396-
const selectedNfts: NFTInfo | undefined = selection?.items;
397-
const disabled = !selectedNft;
398-
const dataUrl = selectedNft?.dataUris?.[0];
396+
397+
const selectedNfts: (NFTInfo & { $nftId: string})[] = selection?.items || [];
398+
399399
const openDialog = useOpenDialog();
400400
const [, setSelectedNFTIds] = useLocalStorage('gallery-selected-nfts', []);
401401

402+
const downloadable = selectedNfts.filter((nft) => !!nft.dataUris?.length);
403+
402404
async function handleDownload() {
403405
const { ipcRenderer } = window as any;
404-
if (!selectedNft) {
406+
if (!downloadable.length) {
405407
return;
406408
}
407409

408-
if (selectedNfts.length > 1) {
409-
const folder = await ipcRenderer.invoke('selectMultipleDownloadFolder');
410-
if (folder?.canceled !== true) {
411-
const nfts = selectedNfts.map((nft: NFTInfo) => {
412-
let hash;
413-
try {
414-
const item = localStorage.getItem(`content-cache-${nft.$nftId}`) || '';
415-
const obj = JSON.parse(item);
416-
if (obj.valid && obj.binary) {
417-
hash = obj.binary;
418-
}
419-
} catch (e) {
420-
return nft;
421-
}
422-
return { ...nft, hash };
423-
});
424-
setSelectedNFTIds([]);
425-
ipcRenderer.invoke('startMultipleDownload', { folder: folder.filePaths[0], nfts });
426-
await openDialog(<MultipleDownloadDialog folder={folder.filePaths[0]} />);
427-
}
428-
} else {
429-
const dataUrlLocal = selectedNft?.dataUris?.[0];
410+
if (downloadable.length === 1) {
411+
const [first] = downloadable;
412+
const dataUrlLocal = first.dataUris?.[0];
430413
if (dataUrlLocal) {
431414
download(dataUrlLocal);
432415
}
416+
return;
417+
}
418+
419+
const folder = await ipcRenderer.invoke('selectMultipleDownloadFolder');
420+
if (folder?.canceled) {
421+
return;
422+
}
423+
424+
setSelectedNFTIds([]);
425+
426+
try {
427+
const selectedFolder = folder.filePaths[0];
428+
if (!selectedFolder) {
429+
throw new Error('No folder selected');
430+
}
431+
432+
const tasks: { url: string; filename: string }[] = selectedNfts.map((nft) => {
433+
const url = nft.dataUris[0];
434+
if (!url) {
435+
throw new Error('No data URI found for NFT');
436+
}
437+
438+
const nftId = nft.$nftId || toBech32m(nft.launcherId, 'nft')
439+
440+
const ext = getFileExtension(url);
441+
const filename = ext ? `${nftId}.${ext}` : nftId;
442+
443+
return {
444+
url,
445+
filename,
446+
};
447+
});
448+
449+
ipcRenderer.invoke('startMultipleDownload', { folder: selectedFolder, tasks });
450+
await openDialog(<MultipleDownloadDialog folder={selectedFolder} />);
451+
} catch (error) {
452+
openDialog(
453+
<AlertDialog title={<Trans>Download Failed</Trans>}>
454+
<Trans>The download failed: {error?.message || 'Unknown error'}</Trans>
455+
</AlertDialog>,
456+
);
433457
}
434458
}
435459

436-
if (!dataUrl) {
460+
if (!downloadable.length) {
437461
return null;
438462
}
439463

440464
return (
441-
<MenuItem onClick={handleDownload} disabled={disabled} close>
465+
<MenuItem onClick={handleDownload} disabled={!downloadable.length} close>
442466
<ListItemIcon>
443467
<DownloadIcon />
444468
</ListItemIcon>

packages/gui/src/electron/CacheManager.ts

+1
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ export default class CacheManager extends EventEmitter {
340340
timeout,
341341
maxSize,
342342
signal: abortController.signal,
343+
overrideFile: true,
343344
});
344345

345346
log('Download finished', url);

packages/gui/src/electron/main.tsx

+31-90
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import {
1414
import fs from 'fs';
1515
import path from 'path';
1616
import url from 'url';
17+
import sanitizeFilename from 'sanitize-filename';
1718

18-
import { NFTInfo } from '@chia-network/api';
1919
import { initialize, enable } from '@electron/remote/main';
2020
import axios from 'axios';
2121
import windowStateKeeper from 'electron-window-state';
@@ -32,6 +32,7 @@ import AppIcon from '../assets/img/chia64x64.png';
3232
import About from '../components/about/About';
3333
import { i18n } from '../config/locales';
3434
import chiaEnvironment, { chiaInit } from '../util/chiaEnvironment';
35+
import downloadFile from './utils/downloadFile';
3536
import loadConfig, { checkConfigFileExists } from '../util/loadConfig';
3637
import manageDaemonLifetime from '../util/manageDaemonLifetime';
3738
import { setUserDataDir } from '../util/userData';
@@ -261,20 +262,7 @@ if (ensureSingleInstance() && ensureCorrectEnvironment()) {
261262
return { err, statusCode, statusMessage, responseBody };
262263
});
263264

264-
function getRemoteFileSize(urlLocal: string): Promise<number> {
265-
return new Promise((resolve, reject) => {
266-
axios({
267-
method: 'HEAD',
268-
url: urlLocal,
269-
})
270-
.then((response) => {
271-
resolve(Number(response.headers['content-length'] || -1));
272-
})
273-
.catch((e) => {
274-
reject(e.message);
275-
});
276-
});
277-
}
265+
278266

279267
ipcMain.handle('showMessageBox', async (_event, options) => dialog.showMessageBox(mainWindow, options));
280268

@@ -334,93 +322,46 @@ if (ensureSingleInstance() && ensureCorrectEnvironment()) {
334322
return responseObj;
335323
});
336324

337-
type DownloadFileWithProgressProps = {
338-
folder: string;
339-
nft: NFTInfo;
340-
current: number;
341-
total: number;
342-
};
343-
344-
function downloadFileWithProgress(props: DownloadFileWithProgressProps): Promise<number> {
345-
const { folder, nft, current, total } = props;
346-
const uri = nft.dataUris[0];
347-
return new Promise((resolve, reject) => {
348-
getRemoteFileSize(uri)
349-
.then((fileSize: number) => {
350-
let totalLength = 0;
351-
currentDownloadRequest = net.request(uri);
352-
currentDownloadRequest.on('response', (response: IncomingMessage) => {
353-
let fileName: string = '';
354-
/* first try to get file name from server headers */
355-
const disposition = response.headers['content-disposition'];
356-
if (disposition && typeof disposition === 'string') {
357-
const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
358-
const matches = filenameRegex.exec(disposition);
359-
if (matches != null && matches[1]) {
360-
fileName = matches[1].replace(/['"]/g, '');
361-
}
362-
}
363-
/* if we didn't get file name from server headers, then parse it from uri */
364-
fileName = fileName || uri.replace(/\/$/, '').split('/').splice(-1, 1)[0];
365-
currentDownloadRequest.on('abort', () => {
366-
reject(new Error('download aborted'));
367-
});
368-
369-
/* if there is already a file with that name in this folder, add nftId to the file name */
370-
if (fs.existsSync(path.join(folder, fileName))) {
371-
fileName = `${fileName}-${nft.$nftId}`;
372-
}
373-
374-
const fileStream = fs.createWriteStream(path.join(folder, fileName));
375-
response.on('data', (chunk) => {
376-
fileStream.write(chunk);
377-
totalLength += chunk.byteLength;
378-
if (fileSize > 0) {
379-
mainWindow?.webContents.send('downloadProgress', {
380-
url: nft.dataUris[0],
381-
nftId: nft.$nftId,
382-
progress: totalLength / fileSize,
383-
i: current,
384-
total,
385-
});
386-
}
387-
});
388-
response.on('end', () => {
389-
if (fileStream) {
390-
fileStream.end();
391-
}
392-
resolve(totalLength);
393-
});
394-
});
395-
currentDownloadRequest.end();
396-
})
397-
.catch((error) => {
398-
reject(error);
399-
});
400-
});
401-
}
402325

403-
ipcMain.handle('startMultipleDownload', async (_event: any, options: any) => {
326+
ipcMain.handle('startMultipleDownload', async (_event: any, options: { folder: string, tasks: { url: string; filename: string }[] }) => {
404327
/* eslint no-await-in-loop: off -- we want to handle each file separately! */
405328
let totalDownloadedSize = 0;
406329
let successFileCount = 0;
407330
let errorFileCount = 0;
408-
for (let i = 0; i < options.nfts.length; i++) {
409-
let fileSize;
331+
332+
const { folder, tasks } = options;
333+
334+
for (let i = 0; i < tasks.length; i++) {
335+
const { url: downloadUrl, filename } = tasks[i];
336+
410337
try {
411-
fileSize = await downloadFileWithProgress({
412-
folder: options.folder,
413-
nft: options.nfts[i],
414-
current: i,
415-
total: options.nfts.length,
338+
const sanitizedFilename = sanitizeFilename(filename);
339+
if (sanitizedFilename !== filename) {
340+
throw new Error(`Filename ${filename} contains invalid characters. Filename sanitized to ${sanitizedFilename}`);
341+
}
342+
343+
const filePath = path.join(folder, sanitizedFilename);
344+
345+
await downloadFile(downloadUrl, filePath, {
346+
onProgress: (progress) => {
347+
mainWindow?.webContents.send('multipleDownloadProgress', {
348+
progress,
349+
url: downloadUrl,
350+
index: i,
351+
total: tasks.length,
352+
});
353+
},
416354
});
417-
totalDownloadedSize += fileSize;
355+
356+
const fileStats = await fs.promises.stat(filePath);
357+
358+
totalDownloadedSize += fileStats.size;
418359
successFileCount++;
419360
} catch (e: any) {
420361
if (e.message === 'download aborted' && abortDownloadingFiles) {
421362
break;
422363
}
423-
mainWindow?.webContents.send('errorDownloadingUrl', options.nfts[i]);
364+
mainWindow?.webContents.send('errorDownloadingUrl', downloadUrl);
424365
errorFileCount++;
425366
}
426367
}

0 commit comments

Comments
 (0)