From a861807a3330e7291df7723d87068d98d3213555 Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Thu, 24 Oct 2019 11:56:31 -0700 Subject: [PATCH 1/4] Add download track feature --- libs/src/api/file.js | 78 +++++++++++--------------- libs/src/api/track.js | 11 ++++ libs/src/services/creatorNode/index.js | 20 +++++++ libs/src/utils.js | 53 +++++++++++++++++ 4 files changed, 116 insertions(+), 46 deletions(-) diff --git a/libs/src/api/file.js b/libs/src/api/file.js index 579fb7e7ddb..7157fd68156 100644 --- a/libs/src/api/file.js +++ b/libs/src/api/file.js @@ -1,57 +1,19 @@ const { Base, Services } = require('./base') -const axios = require('axios') const Utils = require('../utils') -const CancelToken = axios.CancelToken - // Public gateways to send requests to, ordered by precidence. const publicGateways = [ 'https://ipfs.io/ipfs/', 'https://cloudflare-ipfs.com/ipfs/' ] -// Races requests for file content -async function raceRequests ( - urls, - callback -) { - const sources = [] - const requests = urls.map(async (url, i) => { - const source = CancelToken.source() - sources.push(source) - - // Slightly offset requests by their order, so: - // 1. We try public gateways first - // 2. We give requests the opportunity to get canceled if other's are very fast - await Utils.wait(100 * i) - - return new Promise((resolve, reject) => { - axios({ - method: 'get', - url, - responseType: 'blob', - cancelToken: source.token - }) - .then(response => { - resolve({ - blob: response, - url - }) - }) - .catch((thrown) => { - reject(thrown) - // no-op. - // If debugging `axios.isCancel(thrown)` - // can be used to check if the throw was from a cancel. - }) - }) - }) - const response = await Utils.promiseFight(requests) - sources.forEach(source => { - source.cancel('Fetch already succeeded') - }) - callback(response.url) - return response.blob +const downloadURL = (url) => { + if (document) { + const link = document.createElement('a') + link.href = url + link.click() + } + throw new Error('No body document found') } class File extends Base { @@ -70,7 +32,31 @@ class File extends Base { const urls = gateways.map(gateway => `${gateway}${cid}`) try { - return raceRequests(urls, callback) + return Utils.raceRequests(urls, callback, { + method: 'get', + responseType: 'blob' + }) + } catch (e) { + throw new Error(`Failed to retrieve ${cid}`) + } + } + + /** + * Fetches a file from IPFS with a given CID. Follows the same pattern + * as fetchCID, but resolves with a download of the file rather than + * returning the response content. + * @param {string} cid IPFS content identifier + * @param {Array} creatorNodeGateways fallback ipfs gateways from creator nodes + */ + async downloadCID (cid, creatorNodeGateways) { + const gateways = publicGateways + .concat(creatorNodeGateways) + const urls = gateways.map(gateway => `${gateway}${cid}`) + + try { + return Utils.raceRequests(urls, downloadURL, { + method: 'head' + }) } catch (e) { throw new Error(`Failed to retrieve ${cid}`) } diff --git a/libs/src/api/track.js b/libs/src/api/track.js index b7d591f4057..bbe972b0222 100644 --- a/libs/src/api/track.js +++ b/libs/src/api/track.js @@ -1,4 +1,5 @@ const { Base, Services } = require('./base') +const CreatorNode = require('../services/creatorNode') const Utils = require('../utils') const retry = require('async-retry') @@ -69,6 +70,16 @@ class Tracks extends Base { return this.identityService.getTrackListens(timeFrame, idsArray, startTime, endTime, limit, offset) } + /** + * Begins a download for a trackId at the provided endpoints + * @param {string} endpoints user.creator_node_endpoint + * @param {number} trackId the id for the track to download + * @throws if none of the endpoints can provide a download for trackId + */ + async downloadTrack (endpoints, trackId) { + return CreatorNode.downloadTrack(endpoints, trackId) + } + /* ------- SETTERS ------- */ /** diff --git a/libs/src/services/creatorNode/index.js b/libs/src/services/creatorNode/index.js index 641c3f1fe8d..aae5c29cdc0 100644 --- a/libs/src/services/creatorNode/index.js +++ b/libs/src/services/creatorNode/index.js @@ -17,6 +17,26 @@ class CreatorNode { */ static getSecondaries (endpoints) { return endpoints ? endpoints.split(',').slice(1) : [] } + static async checkIfDownloadAvailable (endpoints, trackId) { + for (const endpoint in endpoints.split(',')) { + try { + const res = await axios({ + baseURL: endpoint, + url: '/download/', + params: { + trackId + } + }) + if (res.cid) return res + } catch (e) { + console.log(`Unable to download ${trackId} from ${endpoint}`) + // continue + } + } + // Download is not available, clients should display "processing" + return null + } + /* -------------- */ constructor (web3Manager, creatorNodeEndpoint, isServer, userStateManager, lazyConnect) { diff --git a/libs/src/utils.js b/libs/src/utils.js index dc5a114b349..84fee5f3119 100644 --- a/libs/src/utils.js +++ b/libs/src/utils.js @@ -152,6 +152,59 @@ class Utils { return timings.sort((a, b) => a.millis - b.millis) } + + // Races requests for file content + /** + * Races multiple requests + * @param {*} urls + * @param {*} callback invoked with the first successful url + * @param {object} axiosConfig extra axios config for each request + */ + static async raceRequests ( + urls, + callback, + axiosConfig + ) { + const CancelToken = axios.CancelToken + + const sources = [] + const requests = urls.map(async (url, i) => { + const source = CancelToken.source() + sources.push(source) + + // Slightly offset requests by their order, so: + // 1. We try public gateways first + // 2. We give requests the opportunity to get canceled if other's are very fast + await Utils.wait(100 * i) + + return new Promise((resolve, reject) => { + axios({ + method: 'get', + url, + cancelToken: source.token, + ...axiosConfig + }) + .then(response => { + resolve({ + blob: response, + url + }) + }) + .catch((thrown) => { + reject(thrown) + // no-op. + // If debugging `axios.isCancel(thrown)` + // can be used to check if the throw was from a cancel. + }) + }) + }) + const response = await Utils.promiseFight(requests) + sources.forEach(source => { + source.cancel('Fetch already succeeded') + }) + callback(response.url) + return response.blob + } } module.exports = Utils From bd5f9be609a07ca0d9f26e4b8b9b9fdaa9ec9ef9 Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Fri, 25 Oct 2019 10:29:55 -0700 Subject: [PATCH 2/4] Add docstrings --- libs/src/api/track.js | 13 ++++++------- libs/src/services/creatorNode/index.js | 7 ++++++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/libs/src/api/track.js b/libs/src/api/track.js index bbe972b0222..5821adde374 100644 --- a/libs/src/api/track.js +++ b/libs/src/api/track.js @@ -71,13 +71,12 @@ class Tracks extends Base { } /** - * Begins a download for a trackId at the provided endpoints - * @param {string} endpoints user.creator_node_endpoint - * @param {number} trackId the id for the track to download - * @throws if none of the endpoints can provide a download for trackId - */ - async downloadTrack (endpoints, trackId) { - return CreatorNode.downloadTrack(endpoints, trackId) + * Checks if a download is available from provided creator node endpoints + * @param {string} creatorNodeEndpoints creator node endpoints + * @param {number} trackId + */ + async checkIfDownloadAvailable (trackId, creatorNodeEndpoints) { + return CreatorNode.checkIfDownloadAvailable(trackId, creatorNodeEndpoints) } /* ------- SETTERS ------- */ diff --git a/libs/src/services/creatorNode/index.js b/libs/src/services/creatorNode/index.js index aae5c29cdc0..1f8b8840cd3 100644 --- a/libs/src/services/creatorNode/index.js +++ b/libs/src/services/creatorNode/index.js @@ -17,6 +17,11 @@ class CreatorNode { */ static getSecondaries (endpoints) { return endpoints ? endpoints.split(',').slice(1) : [] } + /** + * Checks if a download is available from provided creator node endpoints + * @param {string} endpoints creator node endpoints + * @param {number} trackId + */ static async checkIfDownloadAvailable (endpoints, trackId) { for (const endpoint in endpoints.split(',')) { try { @@ -27,7 +32,7 @@ class CreatorNode { trackId } }) - if (res.cid) return res + if (res.cid) return res.cid } catch (e) { console.log(`Unable to download ${trackId} from ${endpoint}`) // continue From b21a63d0e864163dbe34d617046d1f67e468075a Mon Sep 17 00:00:00 2001 From: Sid Sethi Date: Tue, 29 Oct 2019 15:30:57 -0700 Subject: [PATCH 3/4] bugfix + nits --- libs/src/services/creatorNode/index.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/libs/src/services/creatorNode/index.js b/libs/src/services/creatorNode/index.js index 1f8b8840cd3..b62171c1cfa 100644 --- a/libs/src/services/creatorNode/index.js +++ b/libs/src/services/creatorNode/index.js @@ -23,14 +23,11 @@ class CreatorNode { * @param {number} trackId */ static async checkIfDownloadAvailable (endpoints, trackId) { - for (const endpoint in endpoints.split(',')) { + for await (const endpoint of endpoints.split(',')) { try { const res = await axios({ baseURL: endpoint, - url: '/download/', - params: { - trackId - } + url: `tracks/download/${trackId}`, }) if (res.cid) return res.cid } catch (e) { From 19cbd7deaec5f63d4e14fe74e687641a155d5128 Mon Sep 17 00:00:00 2001 From: Sid Sethi Date: Wed, 30 Oct 2019 14:32:48 -0700 Subject: [PATCH 4/4] Lint --- libs/src/services/creatorNode/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/src/services/creatorNode/index.js b/libs/src/services/creatorNode/index.js index b62171c1cfa..7f032b031d1 100644 --- a/libs/src/services/creatorNode/index.js +++ b/libs/src/services/creatorNode/index.js @@ -27,7 +27,7 @@ class CreatorNode { try { const res = await axios({ baseURL: endpoint, - url: `tracks/download/${trackId}`, + url: `tracks/download/${trackId}` }) if (res.cid) return res.cid } catch (e) {