node-downloader-helper
Advanced tools
Comparing version 1.0.8 to 1.0.9
@@ -65,8 +65,11 @@ 'use strict'; | ||
_this.url = url; | ||
_this.url = _this.requestURL = url; | ||
_this.state = DH_STATES.IDLE; | ||
_this.__defaultOpts = { | ||
method: 'GET', | ||
headers: {}, | ||
fileName: '', | ||
override: false, | ||
fileName: '' | ||
httpRequestOptions: {}, | ||
httpsRequestOptions: {} | ||
}; | ||
@@ -91,4 +94,4 @@ | ||
_this.__filePath = ''; | ||
_this.__options = _this.__getOptions('GET', url, _this.__opts.headers); | ||
_this.__protocol = url.indexOf('https://') > -1 ? https : http; | ||
_this.__options = _this.__getOptions(_this.__opts.method, url, _this.__opts.headers); | ||
_this.__initProtocol(url); | ||
return _this; | ||
@@ -103,3 +106,3 @@ } | ||
return new Promise(function (resolve, reject) { | ||
if (!_this2.__isRedirected) { | ||
if (!_this2.__isRedirected && _this2.state !== _this2.__states.RESUMED) { | ||
_this2.emit('start'); | ||
@@ -109,92 +112,5 @@ _this2.__setState(_this2.__states.STARTED); | ||
_this2.__request = _this2.__protocol.request(_this2.__options, function (response) { | ||
//Stats | ||
if (!_this2.__isResumed) { | ||
_this2.__total = parseInt(response.headers['content-length']); | ||
_this2.__downloaded = 0; | ||
_this2.__progress = 0; | ||
} | ||
// Start the Download | ||
_this2.__request = _this2.__downloadRequest(resolve, reject); | ||
// Handle Redirects | ||
if (response.statusCode > 300 && response.statusCode < 400 && response.headers.hasOwnProperty('location') && response.headers.location) { | ||
_this2.__isRedirected = true; | ||
_this2.__initProtocol(response.headers.location); | ||
return _this2.start().then(function () { | ||
return resolve(true); | ||
}).catch(function (err) { | ||
_this2.__setState(_this2.__states.FAILED); | ||
_this2.emit('error', err); | ||
return reject(err); | ||
}); | ||
} | ||
// check if response is success | ||
if (response.statusCode !== 200 && response.statusCode !== 206) { | ||
var err = new Error('Response status was ' + response.statusCode); | ||
_this2.emit('error', err); | ||
return reject(err); | ||
} | ||
if (response.headers.hasOwnProperty('accept-ranges') && response.headers['accept-ranges'] !== 'none') { | ||
_this2.__isResumable = true; | ||
} | ||
// Get Filename | ||
if (response.headers.hasOwnProperty('content-disposition') && response.headers['content-disposition'].indexOf('filename=') > -1) { | ||
var fileName = response.headers['content-disposition']; | ||
fileName = fileName.trim(); | ||
fileName = fileName.substr(fileName.indexOf('filename=') + 9); | ||
fileName = fileName.replace(new RegExp('"', 'g'), ''); | ||
_this2.__fileName = fileName; | ||
} else { | ||
_this2.__fileName = path.basename(URL.parse(_this2.url).pathname); | ||
} | ||
// Create File | ||
_this2.__fileName = _this2.__opts.fileName ? _this2.__opts.fileName : _this2.__fileName; | ||
_this2.__filePath = path.join(_this2.__destFolder, _this2.__fileName); | ||
if (!_this2.__opts.override) { | ||
_this2.__filePath = _this2.__uniqFileNameSync(_this2.__filePath); | ||
} | ||
_this2.__fileStream = fs.createWriteStream(_this2.__filePath, _this2.__isResumed ? { 'flags': 'a' } : {}); | ||
// Start Downloading | ||
_this2.emit('download'); | ||
_this2.__isResumed = false; | ||
_this2.__isRedirected = false; | ||
_this2.__setState(_this2.__states.DOWNLOADING); | ||
_this2.__statsEstimate.time = new Date(); | ||
response.pipe(_this2.__fileStream); | ||
response.on('data', function (chunk) { | ||
return _this2.__calculateStats(chunk.length); | ||
}); | ||
_this2.__fileStream.on('finish', function () { | ||
_this2.__fileStream.close(function (_err) { | ||
if (_err) { | ||
return reject(_err); | ||
} | ||
if (_this2.state !== _this2.__states.PAUSED && _this2.state !== _this2.__states.STOPPED) { | ||
_this2.__setState(_this2.__states.FINISHED); | ||
_this2.emit('end'); | ||
} | ||
return resolve(true); | ||
}); | ||
}); | ||
_this2.__fileStream.on('error', function (err) { | ||
_this2.__fileStream.close(function () { | ||
fs.unlink(_this2.__filePath, function () { | ||
return reject(err); | ||
}); | ||
}); | ||
_this2.__setState(_this2.__states.FAILED); | ||
_this2.emit('error', err); | ||
return reject(err); | ||
}); | ||
}); | ||
// Error Handling | ||
@@ -233,4 +149,2 @@ _this2.__request.on('error', function (err) { | ||
value: function resume() { | ||
var _this3 = this; | ||
this.__setState(this.__states.RESUMED); | ||
@@ -243,5 +157,3 @@ if (this.__isResumable) { | ||
this.emit('resume'); | ||
return this.start().then(function () { | ||
return _this3.__isResumable; | ||
}); | ||
return this.start(); | ||
} | ||
@@ -251,3 +163,3 @@ }, { | ||
value: function stop() { | ||
var _this4 = this; | ||
var _this3 = this; | ||
@@ -262,16 +174,16 @@ if (this.__request) { | ||
return new Promise(function (resolve, reject) { | ||
fs.access(_this4.__filePath, function (_accessErr) { | ||
fs.access(_this3.__filePath, function (_accessErr) { | ||
// if can't access, probably is not created yet | ||
if (_accessErr) { | ||
_this4.emit('stop'); | ||
_this3.emit('stop'); | ||
return resolve(true); | ||
} | ||
fs.unlink(_this4.__filePath, function (_err) { | ||
fs.unlink(_this3.__filePath, function (_err) { | ||
if (_err) { | ||
_this4.__setState(_this4.__states.FAILED); | ||
_this4.emit('error', _err); | ||
_this3.__setState(_this3.__states.FAILED); | ||
_this3.emit('error', _err); | ||
return reject(_err); | ||
} | ||
_this4.emit('stop'); | ||
_this3.emit('stop'); | ||
resolve(true); | ||
@@ -283,2 +195,125 @@ }); | ||
}, { | ||
key: 'isResumable', | ||
value: function isResumable() { | ||
return this.__isResumable; | ||
} | ||
}, { | ||
key: '__downloadRequest', | ||
value: function __downloadRequest(resolve, reject) { | ||
var _this4 = this; | ||
return this.__protocol.request(this.__options, function (response) { | ||
//Stats | ||
if (!_this4.__isResumed) { | ||
_this4.__total = parseInt(response.headers['content-length']); | ||
_this4.__downloaded = 0; | ||
_this4.__progress = 0; | ||
} | ||
// Handle Redirects | ||
if (response.statusCode > 300 && response.statusCode < 400 && response.headers.hasOwnProperty('location') && response.headers.location) { | ||
_this4.__isRedirected = true; | ||
_this4.__initProtocol(response.headers.location); | ||
return _this4.start().then(function () { | ||
return resolve(true); | ||
}).catch(function (err) { | ||
_this4.__setState(_this4.__states.FAILED); | ||
_this4.emit('error', err); | ||
return reject(err); | ||
}); | ||
} | ||
// check if response is success | ||
if (response.statusCode !== 200 && response.statusCode !== 206) { | ||
var err = new Error('Response status was ' + response.statusCode); | ||
_this4.emit('error', err); | ||
return reject(err); | ||
} | ||
if (response.headers.hasOwnProperty('accept-ranges') && response.headers['accept-ranges'] !== 'none') { | ||
_this4.__isResumable = true; | ||
} | ||
_this4.__startDownload(response, resolve, reject); | ||
}); | ||
} | ||
}, { | ||
key: '__startDownload', | ||
value: function __startDownload(response, resolve, reject) { | ||
var _this5 = this; | ||
this.__fileName = this.__getFileNameFromHeaders(response.headers); | ||
this.__filePath = this.__getFilePath(this.__fileName); | ||
this.__fileStream = fs.createWriteStream(this.__filePath, this.__isResumed ? { 'flags': 'a' } : {}); | ||
// Start Downloading | ||
this.emit('download'); | ||
this.__isResumed = false; | ||
this.__isRedirected = false; | ||
this.__setState(this.__states.DOWNLOADING); | ||
this.__statsEstimate.time = new Date(); | ||
response.pipe(this.__fileStream); | ||
response.on('data', function (chunk) { | ||
return _this5.__calculateStats(chunk.length); | ||
}); | ||
this.__fileStream.on('finish', function () { | ||
_this5.__fileStream.close(function (_err) { | ||
if (_err) { | ||
return reject(_err); | ||
} | ||
if (_this5.state !== _this5.__states.PAUSED && _this5.state !== _this5.__states.STOPPED) { | ||
_this5.__setState(_this5.__states.FINISHED); | ||
_this5.emit('end'); | ||
} | ||
return resolve(true); | ||
}); | ||
}); | ||
this.__fileStream.on('error', function (err) { | ||
_this5.__fileStream.close(function () { | ||
fs.unlink(_this5.__filePath, function () { | ||
return reject(err); | ||
}); | ||
}); | ||
_this5.__setState(_this5.__states.FAILED); | ||
_this5.emit('error', err); | ||
return reject(err); | ||
}); | ||
} | ||
}, { | ||
key: '__getFileNameFromHeaders', | ||
value: function __getFileNameFromHeaders(headers) { | ||
var fileName = ''; | ||
if (this.__opts.fileName) { | ||
return this.__opts.fileName; | ||
} | ||
// Get Filename | ||
if (headers.hasOwnProperty('content-disposition') && headers['content-disposition'].indexOf('filename=') > -1) { | ||
fileName = headers['content-disposition']; | ||
fileName = fileName.trim(); | ||
fileName = fileName.substr(fileName.indexOf('filename=') + 9); | ||
fileName = fileName.replace(new RegExp('"', 'g'), ''); | ||
} else { | ||
fileName = path.basename(URL.parse(this.requestURL).pathname); | ||
} | ||
return fileName; | ||
} | ||
}, { | ||
key: '__getFilePath', | ||
value: function __getFilePath(fileName) { | ||
var filePath = path.join(this.__destFolder, fileName); | ||
if (!this.__opts.override && this.state !== this.__states.RESUMED) { | ||
filePath = this.__uniqFileNameSync(filePath); | ||
} | ||
return filePath; | ||
} | ||
}, { | ||
key: '__calculateStats', | ||
@@ -369,5 +404,12 @@ value: function __calculateStats(receivedBytes) { | ||
value: function __initProtocol(url) { | ||
this.url = url; | ||
this.__options = this.__getOptions('GET', url, this.__headers); | ||
this.__protocol = url.indexOf('https://') > -1 ? https : http; | ||
var defaultOpts = this.__getOptions(this.__opts.method, url, this.__headers); | ||
this.requestURL = url; | ||
if (url.indexOf('https://') > -1) { | ||
this.__protocol = https; | ||
this.__options = Object.assign({}, defaultOpts, this.__opts.httpsRequestOptions); | ||
} else { | ||
this.__protocol = http; | ||
this.__options = Object.assign({}, defaultOpts, this.__opts.httpRequestOptions); | ||
} | ||
} | ||
@@ -374,0 +416,0 @@ }, { |
@@ -12,5 +12,4 @@ /*eslint no-console: ["error", { allow: ["log", "warn", "error"] }] */ | ||
module.exports.pauseTimer = function (_dl, wait) { | ||
module.exports.pauseResumeTimer = function (_dl, wait) { | ||
setTimeout(() => { | ||
if (_dl.state === DH_STATES.FINISHED || | ||
@@ -24,8 +23,6 @@ _dl.state === DH_STATES.FAILED) { | ||
.then(() => setTimeout(() => { | ||
_dl.resume() | ||
.then(isResumable => { | ||
if (!isResumable) { | ||
console.warn("This URL doesn't support resume, it will start from the beginning"); | ||
} | ||
}); | ||
if (!_dl.isResumable()) { | ||
console.warn("This URL doesn't support resume, it will start from the beginning"); | ||
} | ||
return _dl.resume(); | ||
}, wait)); | ||
@@ -32,0 +29,0 @@ |
/*eslint no-console: ["error", { allow: ["log", "warn", "error"] }] */ | ||
const { DownloaderHelper } = require('../dist'); | ||
const { byteHelper, pauseTimer } = require('./helpers'); | ||
const { byteHelper, pauseResumeTimer } = require('./helpers'); | ||
const url = 'http://ipv4.download.thinkbroadband.com/1GB.zip'; | ||
// Options are optional | ||
const pkg = require('../package.json'); | ||
// these are the default options | ||
const options = { | ||
headers : {}, // http headers ex: 'Authorization' | ||
fileName: '', // custom filename when saved | ||
override: false, //if true it will override the file, otherwise will append '(number)' to the end of file | ||
method: 'GET', // Request Method Verb | ||
// Custom HTTP Header ex: Authorization, User-Agent | ||
headers: { | ||
'user-agent': pkg.name + '@' + pkg.version | ||
}, | ||
fileName: '', // Custom filename when saved | ||
override: false, // if true it will override the file, otherwise will append '(number)' to the end of file | ||
httpRequestOptions: {}, // Override the http request options | ||
httpsRequestOptions: {} // Override the https request options, ex: to add SSL Certs | ||
}; | ||
const dl = new DownloaderHelper(url, __dirname, options); | ||
@@ -18,3 +26,3 @@ | ||
.on('stateChanged', state => console.log('State: ', state)) | ||
.once('download', () => pauseTimer(dl, 5000)) | ||
.once('download', () => pauseResumeTimer(dl, 5000)) | ||
.on('progress', stats => { | ||
@@ -21,0 +29,0 @@ const progress = stats.progress.toFixed(1); |
{ | ||
"name": "node-downloader-helper", | ||
"version": "1.0.8", | ||
"version": "1.0.9", | ||
"description": "A simple http file downloader for node.js", | ||
@@ -5,0 +5,0 @@ "main": "./dist/index.js", |
@@ -15,2 +15,3 @@ # node-downloader-helper | ||
- Supports http redirects | ||
- Support custom native http request options | ||
- Usable on vanilla nodejs, electron, nwjs | ||
@@ -37,3 +38,23 @@ - Progress stats | ||
## Options | ||
Download Helper constructor also allow a 3rd parameter to set some options `constructor(url, destinationFolder, options)`, | ||
these are the default values | ||
```javascript | ||
{ | ||
method: 'GET', // Request Method Verb | ||
headers: {}, // Custom HTTP Header ex: Authorization, User-Agent | ||
fileName: '', // Custom filename when saved | ||
override: false, // if true it will override the file, otherwise will append '(number)' to the end of file | ||
httpRequestOptions: {}, // Override the http request options | ||
httpsRequestOptions: {} // Override the https request options, ex: to add SSL Certs | ||
} | ||
``` | ||
for `httpRequestOptions` the available options are detailed in here https://nodejs.org/api/http.html#http_http_request_options_callback | ||
for `httpsRequestOptions` the available options are detailed in here https://nodejs.org/api/https.html#https_https_request_options_callback | ||
## Methods | ||
@@ -40,0 +61,0 @@ |
246
src/index.js
@@ -27,8 +27,11 @@ import { EventEmitter } from 'events'; | ||
this.url = url; | ||
this.url = this.requestURL = url; | ||
this.state = DH_STATES.IDLE; | ||
this.__defaultOpts = { | ||
method: 'GET', | ||
headers: {}, | ||
fileName: '', | ||
override: false, | ||
fileName: '' | ||
httpRequestOptions: {}, | ||
httpsRequestOptions: {} | ||
}; | ||
@@ -53,6 +56,4 @@ | ||
this.__filePath = ''; | ||
this.__options = this.__getOptions('GET', url, this.__opts.headers); | ||
this.__protocol = (url.indexOf('https://') > -1) | ||
? https | ||
: http; | ||
this.__options = this.__getOptions(this.__opts.method, url, this.__opts.headers); | ||
this.__initProtocol(url); | ||
} | ||
@@ -62,3 +63,4 @@ | ||
return new Promise((resolve, reject) => { | ||
if (!this.__isRedirected) { | ||
if (!this.__isRedirected && | ||
this.state !== this.__states.RESUMED) { | ||
this.emit('start'); | ||
@@ -68,96 +70,5 @@ this.__setState(this.__states.STARTED); | ||
this.__request = this.__protocol.request(this.__options, response => { | ||
//Stats | ||
if (!this.__isResumed) { | ||
this.__total = parseInt(response.headers['content-length']); | ||
this.__downloaded = 0; | ||
this.__progress = 0; | ||
} | ||
// Start the Download | ||
this.__request = this.__downloadRequest(resolve, reject); | ||
// Handle Redirects | ||
if (response.statusCode > 300 && response.statusCode < 400 && | ||
response.headers.hasOwnProperty('location') && response.headers.location) { | ||
this.__isRedirected = true; | ||
this.__initProtocol(response.headers.location); | ||
return this.start() | ||
.then(() => resolve(true)) | ||
.catch(err => { | ||
this.__setState(this.__states.FAILED); | ||
this.emit('error', err); | ||
return reject(err); | ||
}); | ||
} | ||
// check if response is success | ||
if (response.statusCode !== 200 && response.statusCode !== 206) { | ||
const err = new Error('Response status was ' + response.statusCode); | ||
this.emit('error', err); | ||
return reject(err); | ||
} | ||
if (response.headers.hasOwnProperty('accept-ranges') && | ||
response.headers['accept-ranges'] !== 'none') { | ||
this.__isResumable = true; | ||
} | ||
// Get Filename | ||
if (response.headers.hasOwnProperty('content-disposition') && | ||
response.headers['content-disposition'].indexOf('filename=') > -1) { | ||
let fileName = response.headers['content-disposition']; | ||
fileName = fileName.trim(); | ||
fileName = fileName.substr(fileName.indexOf('filename=') + 9); | ||
fileName = fileName.replace(new RegExp('"', 'g'), ''); | ||
this.__fileName = fileName; | ||
} else { | ||
this.__fileName = path.basename(URL.parse(this.url).pathname); | ||
} | ||
// Create File | ||
this.__fileName = (this.__opts.fileName) | ||
? this.__opts.fileName | ||
: this.__fileName; | ||
this.__filePath = path.join(this.__destFolder, this.__fileName); | ||
if (!this.__opts.override) { | ||
this.__filePath = this.__uniqFileNameSync(this.__filePath); | ||
} | ||
this.__fileStream = fs.createWriteStream(this.__filePath, | ||
this.__isResumed ? { 'flags': 'a' } : {}); | ||
// Start Downloading | ||
this.emit('download'); | ||
this.__isResumed = false; | ||
this.__isRedirected = false; | ||
this.__setState(this.__states.DOWNLOADING); | ||
this.__statsEstimate.time = new Date(); | ||
response.pipe(this.__fileStream); | ||
response.on('data', chunk => this.__calculateStats(chunk.length)); | ||
this.__fileStream.on('finish', () => { | ||
this.__fileStream.close(_err => { | ||
if (_err) { | ||
return reject(_err); | ||
} | ||
if (this.state !== this.__states.PAUSED && | ||
this.state !== this.__states.STOPPED) { | ||
this.__setState(this.__states.FINISHED); | ||
this.emit('end'); | ||
} | ||
return resolve(true); | ||
}); | ||
}); | ||
this.__fileStream.on('error', err => { | ||
this.__fileStream.close(() => { | ||
fs.unlink(this.__filePath, () => reject(err)); | ||
}); | ||
this.__setState(this.__states.FAILED); | ||
this.emit('error', err); | ||
return reject(err); | ||
}); | ||
}); | ||
// Error Handling | ||
@@ -199,4 +110,3 @@ this.__request.on('error', err => { | ||
this.emit('resume'); | ||
return this.start() | ||
.then(() => this.__isResumable); | ||
return this.start(); | ||
} | ||
@@ -233,2 +143,117 @@ | ||
isResumable() { | ||
return this.__isResumable; | ||
} | ||
__downloadRequest(resolve, reject) { | ||
return this.__protocol.request(this.__options, response => { | ||
//Stats | ||
if (!this.__isResumed) { | ||
this.__total = parseInt(response.headers['content-length']); | ||
this.__downloaded = 0; | ||
this.__progress = 0; | ||
} | ||
// Handle Redirects | ||
if (response.statusCode > 300 && response.statusCode < 400 && | ||
response.headers.hasOwnProperty('location') && response.headers.location) { | ||
this.__isRedirected = true; | ||
this.__initProtocol(response.headers.location); | ||
return this.start() | ||
.then(() => resolve(true)) | ||
.catch(err => { | ||
this.__setState(this.__states.FAILED); | ||
this.emit('error', err); | ||
return reject(err); | ||
}); | ||
} | ||
// check if response is success | ||
if (response.statusCode !== 200 && response.statusCode !== 206) { | ||
const err = new Error('Response status was ' + response.statusCode); | ||
this.emit('error', err); | ||
return reject(err); | ||
} | ||
if (response.headers.hasOwnProperty('accept-ranges') && | ||
response.headers['accept-ranges'] !== 'none') { | ||
this.__isResumable = true; | ||
} | ||
this.__startDownload(response, resolve, reject); | ||
}); | ||
} | ||
__startDownload(response, resolve, reject) { | ||
this.__fileName = this.__getFileNameFromHeaders(response.headers); | ||
this.__filePath = this.__getFilePath(this.__fileName); | ||
this.__fileStream = fs.createWriteStream(this.__filePath, | ||
this.__isResumed ? { 'flags': 'a' } : {}); | ||
// Start Downloading | ||
this.emit('download'); | ||
this.__isResumed = false; | ||
this.__isRedirected = false; | ||
this.__setState(this.__states.DOWNLOADING); | ||
this.__statsEstimate.time = new Date(); | ||
response.pipe(this.__fileStream); | ||
response.on('data', chunk => this.__calculateStats(chunk.length)); | ||
this.__fileStream.on('finish', () => { | ||
this.__fileStream.close(_err => { | ||
if (_err) { | ||
return reject(_err); | ||
} | ||
if (this.state !== this.__states.PAUSED && | ||
this.state !== this.__states.STOPPED) { | ||
this.__setState(this.__states.FINISHED); | ||
this.emit('end'); | ||
} | ||
return resolve(true); | ||
}); | ||
}); | ||
this.__fileStream.on('error', err => { | ||
this.__fileStream.close(() => { | ||
fs.unlink(this.__filePath, () => reject(err)); | ||
}); | ||
this.__setState(this.__states.FAILED); | ||
this.emit('error', err); | ||
return reject(err); | ||
}); | ||
} | ||
__getFileNameFromHeaders(headers) { | ||
let fileName = ''; | ||
if (this.__opts.fileName) { | ||
return this.__opts.fileName; | ||
} | ||
// Get Filename | ||
if (headers.hasOwnProperty('content-disposition') && | ||
headers['content-disposition'].indexOf('filename=') > -1) { | ||
fileName = headers['content-disposition']; | ||
fileName = fileName.trim(); | ||
fileName = fileName.substr(fileName.indexOf('filename=') + 9); | ||
fileName = fileName.replace(new RegExp('"', 'g'), ''); | ||
} else { | ||
fileName = path.basename(URL.parse(this.requestURL).pathname); | ||
} | ||
return fileName; | ||
} | ||
__getFilePath(fileName) { | ||
let filePath = path.join(this.__destFolder, fileName); | ||
if (!this.__opts.override && this.state !== this.__states.RESUMED) { | ||
filePath = this.__uniqFileNameSync(filePath); | ||
} | ||
return filePath; | ||
} | ||
__calculateStats(receivedBytes) { | ||
@@ -311,10 +336,15 @@ const currentTime = new Date(); | ||
__initProtocol(url) { | ||
this.url = url; | ||
this.__options = this.__getOptions('GET', url, this.__headers); | ||
this.__protocol = (url.indexOf('https://') > -1) | ||
? https | ||
: http; | ||
const defaultOpts = this.__getOptions(this.__opts.method, url, this.__headers); | ||
this.requestURL = url; | ||
if (url.indexOf('https://') > -1) { | ||
this.__protocol = https; | ||
this.__options = Object.assign({}, defaultOpts, this.__opts.httpsRequestOptions); | ||
} else { | ||
this.__protocol = http; | ||
this.__options = Object.assign({}, defaultOpts, this.__opts.httpRequestOptions); | ||
} | ||
} | ||
__uniqFileNameSync(path) { | ||
@@ -321,0 +351,0 @@ if (typeof path !== 'string' || path === '') { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
41601
776
120
15