From 31af2e6e5e92d7f89fce20d5ee4a24aef0960a3e Mon Sep 17 00:00:00 2001 From: Feross Aboukhadijeh Date: Fri, 9 Aug 2019 22:30:43 -0700 Subject: [PATCH] WebTorrent: Add "Save All Files..." feature Currently there is no ability to save all files at once. User has to save each file separately. This commit fixes that. Fixes: https://github.com/brave/brave-browser/issues/1230 --- .../extension/actions/webtorrent_actions.ts | 1 + .../background/reducers/webtorrent_reducer.ts | 15 ++++- .../extension/background/webtorrent.ts | 61 +++++++++++++++++++ .../extension/components/app.tsx | 4 +- .../extension/components/torrentFileList.tsx | 19 +++++- .../extension/components/torrentViewer.tsx | 17 ++++-- .../extension/constants/webtorrent_types.ts | 3 +- .../brave_webtorrent/extension/manifest.json | 2 +- .../extension/styles/styles.css | 8 +++ .../components/torrentViewer_test.tsx | 11 ++-- package-lock.json | 50 ++++++++++++--- package.json | 2 + 12 files changed, 166 insertions(+), 27 deletions(-) diff --git a/components/brave_webtorrent/extension/actions/webtorrent_actions.ts b/components/brave_webtorrent/extension/actions/webtorrent_actions.ts index 86ec9c679f94..f1c70f8939bf 100644 --- a/components/brave_webtorrent/extension/actions/webtorrent_actions.ts +++ b/components/brave_webtorrent/extension/actions/webtorrent_actions.ts @@ -16,3 +16,4 @@ export const serverUpdated = (torrent: Torrent, serverURL: string) => export const startTorrent = (torrentId: string, tabId: number) => action(types.WEBTORRENT_START_TORRENT, { torrentId, tabId }) export const stopDownload = (tabId: number) => action(types.WEBTORRENT_STOP_DOWNLOAD, { tabId }) export const torrentParsed = (torrentId: string, tabId: number, infoHash: string | undefined, errorMsg: string | undefined, parsedTorrent?: Instance) => action(types.WEBTORRENT_TORRENT_PARSED, { torrentId, tabId, infoHash, errorMsg, parsedTorrent }) +export const saveAllFiles = (infoHash: string) => action(types.WEBTORRENT_SAVE_ALL_FILES, { infoHash }) diff --git a/components/brave_webtorrent/extension/background/reducers/webtorrent_reducer.ts b/components/brave_webtorrent/extension/background/reducers/webtorrent_reducer.ts index 4658d89569ec..437932641bed 100644 --- a/components/brave_webtorrent/extension/background/reducers/webtorrent_reducer.ts +++ b/components/brave_webtorrent/extension/background/reducers/webtorrent_reducer.ts @@ -13,7 +13,12 @@ import * as torrentTypes from '../../constants/webtorrent_types' import { File, TorrentState, TorrentsState } from '../../constants/webtorrentState' // Utils -import { addTorrent, delTorrent, findTorrent } from '../webtorrent' +import { + addTorrent, + delTorrent, + findTorrent, + saveAllFiles as webtorrentSaveAllFiles +} from '../webtorrent' import { getTabData } from '../api/tabs_api' import { parseTorrentRemote } from '../api/torrent_api' @@ -224,6 +229,11 @@ const torrentParsed = (torrentId: string, tabId: number, infoHash: string | unde return { ...state, torrentObjMap, torrentStateMap } } +const saveAllFiles = (state: TorrentsState, infoHash: string) => { + webtorrentSaveAllFiles(infoHash) + return { ...state } +} + const defaultState: TorrentsState = { currentWindowId: -1, activeTabIds: {}, torrentStateMap: {}, torrentObjMap: {} } export const webtorrentReducer = (state: TorrentsState = defaultState, action: any) => { // TODO: modify any to be actual action type const payload = action.payload @@ -289,6 +299,9 @@ export const webtorrentReducer = (state: TorrentsState = defaultState, action: a state = torrentParsed(payload.torrentId, payload.tabId, payload.infoHash, payload.errorMsg, payload.parsedTorrent, state) break + case torrentTypes.types.WEBTORRENT_SAVE_ALL_FILES: + state = saveAllFiles(state, payload.infoHash) + break } return state diff --git a/components/brave_webtorrent/extension/background/webtorrent.ts b/components/brave_webtorrent/extension/background/webtorrent.ts index b6a974638638..90c08c35933b 100644 --- a/components/brave_webtorrent/extension/background/webtorrent.ts +++ b/components/brave_webtorrent/extension/background/webtorrent.ts @@ -7,6 +7,8 @@ import { addTorrentEvents, removeTorrentEvents } from './events/torrentEvents' import { addWebtorrentEvents } from './events/webtorrentEvents' import { AddressInfo } from 'net' import { Instance } from 'parse-torrent' +import { basename, extname } from 'path' +import * as JSZip from 'jszip' let webTorrent: WebTorrent.Instance | undefined let servers: { [key: string]: any } = { } @@ -79,3 +81,62 @@ export const delTorrent = (infoHash: string) => { maybeDestroyWebTorrent() } + +export const saveAllFiles = (infoHash: string) => { + const torrent = findTorrent(infoHash) + + if (!torrent || !torrent.name || !torrent.files) return + + const files: WebTorrent.TorrentFile[] = torrent.files + + let zip: any = new JSZip() + const zipFilename = basename(torrent.name, extname(torrent.name)) + '.zip' + + const downloadBlob = (blob: Blob) => { + const url = URL.createObjectURL(blob) + + chrome.downloads.download({ + url: url, + filename: zipFilename, + saveAs: false, + conflictAction: 'uniquify' + }) + } + + const downloadZip = () => { + if (files.length > 1) { + // generate the zip relative to the torrent folder + zip = zip.folder(torrent.name) + } + + zip.generateAsync({ type: 'blob' }) + .then( + (blob: Blob) => downloadBlob(blob), + (err: Error) => console.error(err) + ) + } + + const addFilesToZip = () => { + let addedFiles = 0 + + files.forEach(file => { + file.getBlob((err, blob) => { + if (err) { + console.error(err) + } else { + // add file to zip + zip.file(file.path, blob) + } + + addedFiles += 1 + + // start the download when all files have been added + if (addedFiles === files.length) { + downloadZip() + } + }) + }) + } + + addFilesToZip() +} diff --git a/components/brave_webtorrent/extension/components/app.tsx b/components/brave_webtorrent/extension/components/app.tsx index 8bdcca442c29..36c4c3f4e586 100644 --- a/components/brave_webtorrent/extension/components/app.tsx +++ b/components/brave_webtorrent/extension/components/app.tsx @@ -60,10 +60,8 @@ export class BraveWebtorrentPage extends React.Component { ) } diff --git a/components/brave_webtorrent/extension/components/torrentFileList.tsx b/components/brave_webtorrent/extension/components/torrentFileList.tsx index 14d641502994..a90b4397e880 100644 --- a/components/brave_webtorrent/extension/components/torrentFileList.tsx +++ b/components/brave_webtorrent/extension/components/torrentFileList.tsx @@ -13,12 +13,13 @@ import { File, TorrentObj } from '../constants/webtorrentState' interface Props { torrentId: string - torrent?: TorrentObj + torrent?: TorrentObj, + onSaveAllFiles: () => void } export default class TorrentFileList extends React.PureComponent { render () { - const { torrent, torrentId } = this.props + const { torrent, torrentId, onSaveAllFiles } = this.props if (!torrent || !torrent.files) { return (
@@ -89,9 +90,23 @@ export default class TorrentFileList extends React.PureComponent { } }) + const saveAllFiles = () => { + if (!torrent.serverURL || !torrent.files || torrent.progress !== 1) { + return + } + onSaveAllFiles() + } + return (
+ + Save All Files... +
diff --git a/components/brave_webtorrent/extension/components/torrentViewer.tsx b/components/brave_webtorrent/extension/components/torrentViewer.tsx index cd7f10c9330c..72640525ca32 100644 --- a/components/brave_webtorrent/extension/components/torrentViewer.tsx +++ b/components/brave_webtorrent/extension/components/torrentViewer.tsx @@ -11,20 +11,21 @@ import TorrentFileList from './torrentFileList' import TorrentViewerFooter from './torrentViewerFooter' // Constants -import { TorrentObj } from '../constants/webtorrentState' +import { TorrentObj, TorrentState } from '../constants/webtorrentState' interface Props { actions: any - tabId: number name?: string | string[] - torrentId: string - errorMsg?: string torrent?: TorrentObj + torrentState: TorrentState } export default class TorrentViewer extends React.PureComponent { render () { - const { actions, tabId, name, torrentId, torrent, errorMsg } = this.props + const { actions, name, torrent, torrentState } = this.props + const { torrentId, tabId, errorMsg, infoHash } = torrentState + + const onSaveAllFiles = () => actions.saveAllFiles(infoHash) return (
@@ -37,7 +38,11 @@ export default class TorrentViewer extends React.PureComponent { onStopDownload={actions.stopDownload} /> - +
) diff --git a/components/brave_webtorrent/extension/constants/webtorrent_types.ts b/components/brave_webtorrent/extension/constants/webtorrent_types.ts index 9dfd9faf3d49..6ea3c9588e15 100644 --- a/components/brave_webtorrent/extension/constants/webtorrent_types.ts +++ b/components/brave_webtorrent/extension/constants/webtorrent_types.ts @@ -8,5 +8,6 @@ export const enum types { WEBTORRENT_SERVER_UPDATED = '@@webtorrent/WEBTORRENT_SERVER_UPDATED', WEBTORRENT_START_TORRENT = '@@webtorrent/WEBTORRENT_START_TORRENT', WEBTORRENT_STOP_DOWNLOAD = '@@webtorrent/WEBTORRENT_STOP_DOWNLOAD', - WEBTORRENT_TORRENT_PARSED = '@@webtorrent/WEBTORRENT_TORRENT_PARSED' + WEBTORRENT_TORRENT_PARSED = '@@webtorrent/WEBTORRENT_TORRENT_PARSED', + WEBTORRENT_SAVE_ALL_FILES = '@@webtorrent/WEBTORRENT_SAVE_ALL_FILES' } diff --git a/components/brave_webtorrent/extension/manifest.json b/components/brave_webtorrent/extension/manifest.json index f298a0c54bd4..2f46c6f5c3e2 100644 --- a/components/brave_webtorrent/extension/manifest.json +++ b/components/brave_webtorrent/extension/manifest.json @@ -7,7 +7,7 @@ "scripts": ["extension/out/brave_webtorrent_background.bundle.js"], "persistent": true }, - "permissions": ["dns", "tabs", "windows", ""], + "permissions": ["downloads", "dns", "tabs", "windows", ""], "sockets": { "udp": { "send": "*", diff --git a/components/brave_webtorrent/extension/styles/styles.css b/components/brave_webtorrent/extension/styles/styles.css index 1be10845e1a1..609301203ab3 100644 --- a/components/brave_webtorrent/extension/styles/styles.css +++ b/components/brave_webtorrent/extension/styles/styles.css @@ -97,3 +97,11 @@ a { #iframe { border: 0; } + +a.inactive { + color: gray; +} + +a.inactive:hover { + cursor: default; +} diff --git a/components/test/brave_webtorrent/components/torrentViewer_test.tsx b/components/test/brave_webtorrent/components/torrentViewer_test.tsx index 87496ec0f9be..bdf7c1666707 100644 --- a/components/test/brave_webtorrent/components/torrentViewer_test.tsx +++ b/components/test/brave_webtorrent/components/torrentViewer_test.tsx @@ -10,14 +10,17 @@ import TorrentViewer from '../../../brave_webtorrent/extension/components/torren describe('torrentViewer component', () => { describe('torrentViewer dumb component', () => { it('renders the component', () => { - const tabId = 1 - const torrentId = 'id' + const torrentState: TorrentState = { + tabId: 1, + torrentId: 'id' + } + const name = 'name' const wrapper = shallow( ) const assertion = wrapper.find('.torrent-viewer') diff --git a/package-lock.json b/package-lock.json index 58fee9e8e1a3..e76ed369edb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3604,6 +3604,14 @@ "integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==", "dev": true }, + "@types/jszip": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/jszip/-/jszip-3.1.6.tgz", + "integrity": "sha512-m8uFcI+O2EupCfbEVQWsBM/4nhbegjOHL7cQgBpM95FeF98kdFJXzy9/8yhx4b3lCRl/gMBhcvyh30Qt3X+XPQ==", + "requires": { + "@types/node": "*" + } + }, "@types/magnet-uri": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/magnet-uri/-/magnet-uri-5.1.2.tgz", @@ -6605,8 +6613,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "corejs-upgrade-webpack-plugin": { "version": "2.1.0", @@ -9997,6 +10004,11 @@ "dev": true, "optional": true }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, "immediate-chunk-store": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/immediate-chunk-store/-/immediate-chunk-store-2.0.0.tgz", @@ -10488,8 +10500,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -11307,6 +11318,17 @@ "verror": "1.10.0" } }, + "jszip": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.2.tgz", + "integrity": "sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + } + }, "junk": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", @@ -11494,6 +11516,14 @@ "type-check": "~0.3.2" } }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, "load-ip-set": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/load-ip-set/-/load-ip-set-2.1.0.tgz", @@ -12827,8 +12857,7 @@ "pako": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.8.tgz", - "integrity": "sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA==", - "dev": true + "integrity": "sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA==" }, "parallel-transform": { "version": "1.1.0", @@ -13365,8 +13394,7 @@ "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "promise": { "version": "7.3.1", @@ -14333,7 +14361,6 @@ "version": "2.3.6", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -15037,6 +15064,11 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, "set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", diff --git a/package.json b/package.json index 18baf0e52d01..280b649b0f1a 100644 --- a/package.json +++ b/package.json @@ -328,11 +328,13 @@ "webpack-cli": "^3.0.8" }, "dependencies": { + "@types/jszip": "^3.1.6", "@types/parse-torrent": "^5.8.3", "@types/webtorrent": "^0.98.5", "bignumber.js": "^7.2.1", "bluebird": "^3.5.1", "clipboard-copy": "^2.0.0", + "jszip": "^3.2.2", "prettier-bytes": "^1.0.4", "qr-image": "^3.2.0", "react-chrome-redux": "^1.5.1",