├── .gitattributes ├── test ├── fixture.js ├── setup.js ├── mock-client.js ├── mock-config.js ├── file-test.js └── cdnup-test.js ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── documentation.md │ ├── bug_report.md │ └── regression.md ├── LICENSE └── PULL_REQUEST_TEMPLATE.md ├── .travis.yml ├── .gitignore ├── CHANGELOG.md ├── SECURITY.md ├── LICENSE ├── package.json ├── file.js ├── index.js ├── CONTRIBUTING.md └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | package-lock.json binary 2 | -------------------------------------------------------------------------------- /test/fixture.js: -------------------------------------------------------------------------------- 1 | module.exports = 'hi there'; 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "godaddy", 3 | "rules": { 4 | "strict": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 👉 Please follow one of these issue templates https://github.com/warehouseai/cdnup/issues/new/choose 2 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | process.env.AWS_ACCESS_KEY_ID = 'whatever'; 2 | process.env.AWS_SECRET_ACCESS_KEY = 'whaatever'; 3 | process.env.AWS_DEFAULT_REGION = 'us-east-1'; 4 | -------------------------------------------------------------------------------- /test/mock-client.js: -------------------------------------------------------------------------------- 1 | const { PassThrough } = require('stream'); 2 | 3 | class Client { 4 | _write() {} 5 | upload() { 6 | const stream = new PassThrough(); 7 | setImmediate(() => { 8 | stream.emit('success'); 9 | }); 10 | return stream; 11 | } 12 | } 13 | 14 | module.exports = Client; 15 | -------------------------------------------------------------------------------- /test/mock-config.js: -------------------------------------------------------------------------------- 1 | const endpoint = 'http://localhost:4572'; 2 | const bucket = 'test-cdnup'; 3 | 4 | module.exports = { 5 | check: `${ endpoint }/${ bucket }/`, 6 | acl: 'public-read', 7 | prefix: bucket, 8 | pkgcloud: { 9 | accessKeyId: 'fakeId', 10 | secretAccessKey: 'fakeKey', 11 | provider: 'amazon', 12 | forcePathBucket: true, 13 | endpoint 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 📃 Documentation Bug 3 | about: You want to report something that is wrong or missing from the documentation. 4 | labels: "Type: Docs" 5 | --- 6 | 7 | ## 📃 Summary 8 | 11 | 12 | ## Expected documentation 13 | 16 | -------------------------------------------------------------------------------- /.github/LICENSE: -------------------------------------------------------------------------------- 1 | `ISSUE_TEMPLATE.md` and markdown files under the `ISSUE_TEMPLATE` directory are adapted from `react-native` under MIT. 2 | 3 | https://github.com/facebook/react-native/blob/master/.github/ISSUE_TEMPLATE.md 4 | https://github.com/facebook/react-native/blob/37bf2ce/.github/ISSUE_TEMPLATE.md 5 | 6 | https://github.com/facebook/react-native/tree/master/.github/ISSUE_TEMPLATE 7 | https://github.com/facebook/react-native/tree/37bf2ce/.github/ISSUE_TEMPLATE 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: node_js 3 | 4 | services: 5 | - docker 6 | 7 | node_js: 8 | - "10" 9 | - "12" 10 | 11 | env: 12 | - DEBUG=cdn* AWS_ACCESS_KEY_ID=foobar AWS_SECRET_ACCESS_KEY=foobar 13 | 14 | before_install: 15 | - docker pull localstack/localstack 16 | - docker run -d -e SERVICES=s3 -p 127.0.0.1:4572:4572 --name localstack localstack/localstack 17 | 18 | install: npm install 19 | 20 | matrix: 21 | fast_finish: true 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | test/config.js 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 4.1.0 4 | 5 | - Add uploadOpts option 6 | - Enable travis CI appropriately 7 | - Make tests work locally more out of the box 8 | 9 | ### 4.0.1 10 | 11 | - [#16] Ensure unit tests can run and fix error detection for missing bucket 12 | 13 | ### 4.0.0 14 | 15 | - [#13] new file.contentEncoding API to replace file.contentType 16 | - [#12] Add collected documentation and badges. 17 | - Fixes issue [#8] 18 | - Document missing API methods 19 | - Update dependencies 20 | 21 | [#8]: https://github.com/warehouseai/cdnup/issues/8 22 | 23 | [#12]: https://github.com/warehouseai/cdnup/pull/12 24 | [#13]: https://github.com/warehouseai/cdnup/pull/13 25 | [#16]: https://github.com/warehouseai/cdnup/pull/16 26 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## Summary 8 | 9 | 13 | 14 | ## Changelog 15 | 16 | 20 | 21 | ## Test Plan 22 | 23 | 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: You want to report a reproducible bug. 4 | labels: "Type: Bug Report" 5 | --- 6 | 7 | ## 🐛 Bug Report 8 | 12 | 13 | ## To Reproduce 14 | 17 | 18 | ## Expected Behavior 19 | 22 | 23 | ## Code Example 24 | 30 | 31 | ## Environment 32 | 35 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | We take security very seriously at GoDaddy. We appreciate your efforts to 4 | responsibly disclose your findings, and will make every effort to acknowledge 5 | your contributions. 6 | 7 | ## Where should I report security issues? 8 | 9 | In order to give the community time to respond and upgrade, we strongly urge you 10 | report all security issues privately. 11 | 12 | To report a security issue in one of our Open Source projects email us directly 13 | at **oss@godaddy.com** and include the word "SECURITY" in the subject line. 14 | 15 | This mail is delivered to our Open Source Security team. 16 | 17 | After the initial reply to your report, the team will keep you informed of the 18 | progress being made towards a fix and announcement, and may ask for additional 19 | information or guidance. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/regression.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💥 Regression Report 3 | about: You want to report unexpected behavior that worked in previous releases. 4 | labels: "Type: Bug Report", "Impact: Regression" 5 | --- 6 | 7 | ## 💥 Regression Report 8 | 11 | 12 | ## Last working version 13 | 14 | Worked up to version: 15 | 16 | Stopped working in version: 17 | 18 | ## To Reproduce 19 | 20 | 23 | 24 | ## Expected Behavior 25 | 26 | 29 | 30 | ## Code Example 31 | 37 | 38 | ## Environment 39 | 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 GoDaddy Operating Company, LLC. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdnup", 3 | "version": "4.1.0", 4 | "description": "CDN Uploading for everyone", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha -r test/setup.js test/*-test.js", 8 | "eslint": "eslint-godaddy -c .eslintrc test/ ./*.js", 9 | "posttest": "npm run eslint", 10 | "localstack": "docker run -it -e SERVICES=s3 -p 4572:4572 --rm localstack/localstack" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "github.com/warehouseai/cdnup" 15 | }, 16 | "keywords": [ 17 | "cdn", 18 | "upload", 19 | "assets" 20 | ], 21 | "author": "GoDaddy Operating Company LLC", 22 | "license": "MIT", 23 | "dependencies": { 24 | "backo": "~1.1.0", 25 | "diagnostics": "1.0.x", 26 | "eventemitter3": "1.1.x", 27 | "mime": "^1.6.0", 28 | "mkdirp": "0.5.x", 29 | "one-time": "0.0.4", 30 | "pkgcloud": "^2.1.0", 31 | "reads": "~1.0.1" 32 | }, 33 | "devDependencies": { 34 | "assume": "1.4.x", 35 | "assume-sinon": "^1.0.1", 36 | "aws-liveness": "^1.1.1", 37 | "clone": "^2.1.2", 38 | "eslint": "^5.16.0", 39 | "eslint-config-godaddy": "^3.0.0", 40 | "eslint-plugin-json": "^1.4.0", 41 | "eslint-plugin-mocha": "^5.3.0", 42 | "mocha": "^5.2.0", 43 | "sinon": "^9.0.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/file-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | describe('File', function () { 5 | const Client = require('./mock-client'); 6 | const sinon = require('sinon'); 7 | const path = require('path'); 8 | 9 | var File = require('../file'); 10 | var assume = require('assume'); 11 | 12 | assume.use(require('assume-sinon')); 13 | 14 | describe('uploadOpts', function () { 15 | it('it spreads uploadOpts as parameters to pkgcloud upload', function (next) { 16 | var file = new File(1, { 17 | client: new Client(), 18 | acl: 'public-read', 19 | bucket: 'what' 20 | }, { 21 | uploadOpts: { 22 | cacheControl: 'max-age=10' 23 | } 24 | }); 25 | const spy = sinon.spy(file.client, 'upload'); 26 | 27 | file.create(path.join(__dirname, '/fixture.js'), 'other.js', (err) => { 28 | assume(err).is.falsey(); 29 | assume(spy).is.called(1); 30 | assume(spy.getCall(0).args[0].cacheControl).equals('max-age=10'); 31 | next(); 32 | }); 33 | 34 | }); 35 | }); 36 | 37 | describe('#contentType', function () { 38 | it('looks up the content type', function () { 39 | var file = new File(5, {}, {}); 40 | 41 | assume(file.contentDetect('hello.js').type).equals('application/javascript'); 42 | assume(file.contentDetect('hello.html').type).equals('text/html'); 43 | assume(file.contentDetect('hello.dfadfasdf').type).equals('application/octet-stream'); 44 | assume(file.contentDetect('hello.js.gz')).eql({ type: 'application/javascript', enc: 'gzip' }); 45 | }); 46 | 47 | it('allows prefers override over `mime` library', function () { 48 | var file = new File(5, {}, { 49 | mime: { 50 | '.svgs': 'text/plain', 51 | '.html': 'fake/news' 52 | } 53 | }); 54 | 55 | assume(file.contentDetect('hello.html').type).equals('fake/news'); 56 | assume(file.contentDetect('hello.svgs').type).equals('text/plain'); 57 | }); 58 | }); 59 | }); 60 | 61 | 62 | -------------------------------------------------------------------------------- /file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-invalid-this: 0 */ 4 | 5 | var debug = require('diagnostics')('cdn:file'); 6 | var Backoff = require('backo'); 7 | var one = require('one-time'); 8 | var reads = require('reads'); 9 | var path = require('path'); 10 | var mime = require('mime'); 11 | 12 | /** 13 | * Representation of a single file operation for the CDN. 14 | * 15 | * @constructor 16 | * @param {Number} retries Amount of retries. 17 | * @param {CDNUp} cdn CDN reference. 18 | * @param {Object} options Additional configuration. 19 | * @api private 20 | */ 21 | function File(retries, cdn, options) { 22 | options = options || {}; 23 | 24 | this.backoff = new Backoff({ min: 100, max: 20000 }); 25 | this.uploadOpts = options.uploadOpts || {}; 26 | this.mime = options.mime || {}; 27 | this.retries = retries || 5; 28 | this.client = cdn.client; 29 | this.cdn = cdn; 30 | } 31 | 32 | /** 33 | * Find the correct contentType and optional contentEncoding for a given filename. 34 | * 35 | * @param {String} filename Name of the file. 36 | * @returns {String} The content type. 37 | * @api public 38 | */ 39 | File.prototype.contentDetect = function contentType(filename) { 40 | const { ext, name } = path.parse(filename); 41 | const gz = ext === '.gz'; 42 | let type, enc; 43 | 44 | // 45 | // If we're provided with a custom mime lookup object we should prefer that 46 | // over the mime library; 47 | // 48 | if (ext in this.mime) type = this.mime[ext]; 49 | 50 | // Unlikely to conficlt with overrides above. Includes properly setting the 51 | // contentType and contentEncoding for given gzipped files 52 | if (gz && name.includes('.')) { 53 | type = mime.lookup(name); 54 | enc = 'gzip'; 55 | } 56 | 57 | type = type || mime.lookup(ext); 58 | 59 | return { type, enc }; 60 | }; 61 | 62 | 63 | /** 64 | * Create a new file on the CDN. 65 | * 66 | * @param {String} what Thing that needs to be written. 67 | * @param {String} as Target filename. 68 | * @param {Function} fn Completion callback. 69 | * @api private 70 | */ 71 | File.prototype.create = function create(what, as, fn) { 72 | var file = this; 73 | 74 | this.attempt(function attempt(next) { 75 | debug('attempting to write file to cdn: %s', as); 76 | 77 | const { type, enc } = file.contentDetect(as); 78 | const opts = { 79 | acl: file.cdn.acl, 80 | container: file.cdn.bucket, 81 | remote: as, 82 | ...file.uploadOpts 83 | }; 84 | 85 | if (type) opts.contentType = type; 86 | if (enc) opts.contentEncoding = enc; 87 | 88 | reads(what) 89 | .pipe(file.client.upload(opts)) 90 | .once('error', next) 91 | .once('success', next.bind(null, null)); 92 | }, fn); 93 | }; 94 | 95 | /** 96 | * Poor mans retry handler. 97 | * 98 | * @param {Function} action Function that needs to do something that can be retried. 99 | * @param {Function} fn Completion callback if we run out of retries. 100 | * @returns {Object} the result of calling fn with the specified 'this' value 101 | * @api private 102 | */ 103 | File.prototype.attempt = function attempt(action, fn) { 104 | var file = this; 105 | 106 | if (!file.retries) return fn(new Error('Max retries exhausted')); 107 | if (file.retries) action(one(function next(err) { 108 | if (!err) return fn.apply(this, arguments); 109 | 110 | if (err && err.code === 'NoSuchBucket') { 111 | return file.cdn.init((error) => { 112 | if (error) return fn(error); 113 | 114 | file.attempt(action, fn); 115 | }); 116 | } 117 | // 118 | // Other failure, which could be totally random so we should try again in 119 | // due time. 120 | // 121 | file.retries--; 122 | debug('recieved a failed attempt, we have %d retries left', file.retries); 123 | setTimeout(() => file.attempt(action, fn), file.backoff.duration()); 124 | })); 125 | }; 126 | 127 | // 128 | // Expose the File instance. 129 | // 130 | module.exports = File; 131 | -------------------------------------------------------------------------------- /test/cdnup-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint max-nested-callbacks: 0 */ 4 | /* eslint no-invalid-this: 0 */ 5 | 6 | describe('cdnup', function () { 7 | this.timeout(60000); 8 | var AwsLiveness = require('aws-liveness'); 9 | var { S3 } = require('aws-sdk'); 10 | var assume = require('assume'); 11 | var clone = require('clone'); 12 | var CDNUp = require('..'); 13 | var resolve = require('url').resolve; 14 | var fixture = require('path').resolve(__dirname, 'fixture.js'); 15 | var config = require('./mock-config'); 16 | var root = config.prefix || 'cdnup'; 17 | 18 | // 19 | // Define a local var so we override it. 20 | // 21 | var cdnup; 22 | 23 | function subdomainConfig() { 24 | const conf = clone(config); 25 | conf.pkgcloud.forcePathBucket = false; 26 | conf.subdomain = true; 27 | 28 | return conf; 29 | } 30 | cdnup = new CDNUp(root, config); 31 | before(async function () { 32 | await new AwsLiveness().waitForServices({ 33 | clients: [new S3(cdnup.client._awsConfig)], 34 | waitSeconds: 60 35 | }); 36 | }); 37 | 38 | beforeEach(function () { 39 | cdnup = new CDNUp(root, config); 40 | }); 41 | 42 | it('exports as a function', function () { 43 | assume(CDNUp).is.a('function'); 44 | }); 45 | 46 | describe('#init', function () { 47 | it('inits the bucket', function (next) { 48 | cdnup.init(function (err) { 49 | if (err) return next(err); 50 | next(); 51 | }); 52 | }); 53 | }); 54 | 55 | describe('uploadOpts', function () { 56 | var cdn = new CDNUp(root, { 57 | ...clone(config), 58 | uploadOpts: { 59 | cacheControl: 'max-age=1209600' 60 | } 61 | }); 62 | 63 | it('sets uploadOpts', function () { 64 | assume(cdn.uploadOpts.cacheControl).equals('max-age=1209600'); 65 | }); 66 | }); 67 | 68 | describe('#upload', function () { 69 | it('creates a connection with the server', function (next) { 70 | var name = 'uploaded-fixture.js'; 71 | cdnup.upload(fixture, name, function (err) { 72 | if (err) return next(err); 73 | 74 | cdnup.client.getFiles(cdnup.bucket, (error, files) => { 75 | if (error) return next(error); 76 | var filtered = files.filter(f => f.name === name); 77 | assume(filtered.length).equals(1); 78 | next(); 79 | }); 80 | }); 81 | }); 82 | 83 | it('uploads fixture with path in fixture name', function (next) { 84 | var name = 'fingerprint/uploaded-fixture.js'; 85 | cdnup.upload(fixture, name, function (err) { 86 | if (err) return next(err); 87 | 88 | cdnup.client.getFiles(cdnup.bucket, (error, files) => { 89 | if (error) return next(error); 90 | var filtered = files.filter(f => f.name === name); 91 | assume(filtered.length).equals(1); 92 | next(); 93 | }); 94 | }); 95 | }); 96 | 97 | it('returns a url with the location of the file', function (next) { 98 | cdnup.upload(fixture, 'hello-fixture.js', function (err, url) { 99 | if (err) return next(err); 100 | 101 | assume(url).equals(resolve(cdnup.url(), 'hello-fixture.js')); 102 | next(); 103 | }); 104 | }); 105 | 106 | it('supports subdomain/true option for the URL it produces', function () { 107 | const conf = subdomainConfig(); 108 | const cdn = new CDNUp(conf.prefix, conf); 109 | const uri = resolve(cdn.url(), 'hello-fixture.js'); 110 | 111 | assume(uri).startsWith(`https://${cdnup.bucket}`); 112 | }); 113 | 114 | it('supports check URL replacement', function () { 115 | var conf = subdomainConfig(); 116 | conf.check = `https://${conf.prefix}.s3.amazonaws.com/`; 117 | conf.url = `https://whatever.com/world`; 118 | const cdn = new CDNUp(conf.prefix, conf); 119 | const what = `https://whatever.com/world/hello-fixture.js`; 120 | assume(cdn.checkUrl(what)).contains(conf.check); 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('diagnostics')('cdn:up'); 4 | var EventEmitter = require('eventemitter3'); 5 | var File = require('./file'); 6 | var url = require('url'); 7 | var pkgcloud = require('pkgcloud'); 8 | 9 | /** 10 | * CDNup is our CDN management API. 11 | * 12 | * Options: 13 | * 14 | * - sharding: Use DNS sharding. 15 | * - env: Optional forced environment. 16 | * - url/urls: Array or string of a URL that we use to build our assets URLs 17 | * - mime: Custom lookup object. 18 | * 19 | * @constructor 20 | * @param {String} bucket bucket location of where you want the files to be stored. 21 | * @param {Object} options Configuration for the CDN uploading. 22 | * @api public 23 | */ 24 | function CDNUp(bucket, options) { 25 | options = options || {}; 26 | 27 | this.sharding = !!options.sharding; 28 | this.urls = arrayify(options.url || options.urls); 29 | this.mime = options.mime || {}; 30 | this.check = options.check; 31 | this.bucket = bucket; 32 | this.client = pkgcloud.storage.createClient(options.pkgcloud || {}); 33 | this.acl = options.acl; 34 | this.subdomain = options.subdomain; 35 | this.uploadOpts = options.uploadOpts || {}; 36 | } 37 | 38 | // 39 | // Inherit from the event emitter so we can more easily queue callbacks to 40 | // prevent duplicate mounting. 41 | // 42 | CDNUp.prototype = new EventEmitter(); 43 | CDNUp.prototype.constructor = CDNUp; 44 | 45 | /** 46 | * Init the bucket for the storage container 47 | * 48 | * @param {Function} fn Completion callback 49 | * @returns {CDNUp} The current instance (for fluent/chaining API). 50 | * @api public 51 | */ 52 | CDNUp.prototype.init = function init(fn) { 53 | var cdn = this; 54 | 55 | if (cdn.listeners('init').length) return cdn.once('init', fn); 56 | debug('init container %s', this.bucket); 57 | 58 | this.client.createContainer(this.bucket, cdn.emit.bind(cdn, 'init')); 59 | 60 | return cdn.once('init', fn); 61 | }; 62 | 63 | /** 64 | * Upload a new file on to the CDN. 65 | * 66 | * @param {Stream|String|Buffer} what Things that needs to uploaded. 67 | * @param {String} as Name for the file. 68 | * @param {Function} fn Completion callback. 69 | * @returns {Object} this The CDN 70 | * @api public 71 | */ 72 | CDNUp.prototype.upload = function upload(what, as, fn) { 73 | var cdn = this; 74 | var file = new File(5, this, { 75 | uploadOpts: this.uploadOpts, 76 | mime: this.mime 77 | }); 78 | 79 | file.create(what, as, function uploaded(err, f) { 80 | if (err) return fn(err); 81 | // 82 | // Yes, yet another url.resolve. This is required because node's URL.resolve 83 | // cannot handle multiple paths. So url.resolve(one, two, tree) fails but 84 | // multiple calls work fine. It's documented, but annoying as fuck. 85 | // 86 | fn(null, url.resolve(cdn.url(f), as)); 87 | }); 88 | 89 | return this; 90 | }; 91 | 92 | /** 93 | * Return the URL and path we are uploading our files to 94 | * 95 | * @returns {String} URL + path to the CDN 96 | * @api public 97 | */ 98 | CDNUp.prototype.url = function () { 99 | // 100 | // Figure out which subdomain/url prefix we want to use so browsers can 101 | // maximize the number of concurrent downloaded assets. 102 | // 103 | var prefix = this.sharding 104 | ? this.urls[Math.floor(Math.random() * this.urls.length)] 105 | : this.urls[0]; 106 | 107 | // Either use the given URL as the root, or if we are using subdomain based 108 | // buckets for our given endpoint, append 109 | prefix = prefix 110 | || (this.client.protocol 111 | + (this.subdomain ? `${this.bucket}.` : '') 112 | + this.client.endpoint); 113 | 114 | // 115 | // Needs to end with `/` or the URL.resolve will replace the last path. 116 | // 117 | var root = prefix; 118 | if (!this.subdomain) root = url.resolve(prefix, this.bucket); 119 | if (root.charAt(root.length - 1) !== '/') root = root + '/'; 120 | 121 | return root; 122 | }; 123 | 124 | /** 125 | * Return the URL of the `file` specified to use when checking for the 126 | * existence of that file within the CDN. If your CDN is behind a firewall 127 | * or other limited network scenario this will be necessary. 128 | * 129 | * @param {String} file File to (potentially) transform. 130 | * @returns {String} URL + path to the CDN for the file 131 | * @api public 132 | */ 133 | CDNUp.prototype.checkUrl = function (file) { 134 | if (!this.check) return file; 135 | 136 | const uri = this.url(); 137 | if (file.includes(uri)) { 138 | return file.replace(uri, this.check); 139 | } 140 | 141 | const parsed = url.parse(file); 142 | return this.check.replace(/\/$/, '') + parsed.pathname; 143 | }; 144 | 145 | // 146 | // Force a single string to an array if necessary 147 | // 148 | function arrayify(urls) { 149 | var tmp = Array.isArray(urls) ? urls : [urls]; 150 | return tmp.filter(Boolean); 151 | } 152 | 153 | // 154 | // Expose the module. 155 | // 156 | module.exports = CDNUp; 157 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Everyone is welcome to contribute to GoDaddy's Open Source Software. 4 | Contributing doesn’t just mean submitting pull requests. To get involved you 5 | can, report or triage bugs and participate in discussions on the evolution of 6 | each project. 7 | 8 | No matter how you want to get involved, we ask that you first learn what’s 9 | expected of anyone who participates in the project by reading the Contribution 10 | Guidelines. 11 | 12 | ## Answering Questions 13 | 14 | One of the most important and immediate ways you can support this project is 15 | to or [Github][issues]. Whether you’re 16 | helping a newcomer understand a feature or troubleshooting an edge case with a 17 | seasoned developer, your knowledge and experience with JS can go a long way 18 | to help others. 19 | 20 | ## Reporting Bugs 21 | 22 | **Do not report potential security vulnerabilities here. Refer to 23 | [SECURITY.md](./SECURITY.md) for more details about the process of reporting 24 | security vulnerabilities.** 25 | 26 | Before submitting a ticket, please be sure to have a simple replication of 27 | the behavior. If the issue is isolated to one of the dependencies of this 28 | project. Please create a Github issue in that project. All dependencies are 29 | open source software and can be easily found through [npm]. 30 | 31 | Submit a ticket for your issue, assuming one does not already exist: 32 | - Create it on our [Issue Tracker][issues] 33 | - Clearly describe the issue by following the template layout 34 | - Make sure to include steps to reproduce the bug. 35 | - A reproducible (unit) test could be helpful in solving the bug. 36 | - Describe the environment that (re)produced the problem. 37 | 38 | > For a bug to be actionable, it needs to be reproducible. If you or 39 | > contributors can’t reproduce the bug, try to figure out why. Please take 40 | > care to stay involved in discussions around solving the problem. 41 | 42 | ## Triaging bugs or contributing code 43 | 44 | If you're triaging a bug, try to reduce it. Once a bug can be reproduced, 45 | reduce it to the smallest amount of code possible. Reasoning about a sample 46 | or unit test that reproduces a bug in just a few lines of code is easier than 47 | reasoning about a longer sample. 48 | 49 | From a practical perspective, contributions are as simple as: 50 | - Forking the repository on GitHub. 51 | - Making changes to your forked repository. 52 | - When committing, reference your issue (if present) and include a note about 53 | the fix. 54 | - If possible, and if applicable, please also add/update unit tests for your 55 | changes. 56 | - Push the changes to your fork and submit a pull request to the 'master' 57 | branch of the projects' repository. 58 | 59 | If you are interested in making a large change and feel unsure about its 60 | overall effect, please make sure to first discuss the change and reach a 61 | consensus with core contributors. Then ask about the best way 62 | to go about making the change. 63 | 64 | ## Code Review 65 | 66 | Any open source project relies heavily on code review to improve software 67 | quality: 68 | 69 | > All significant changes, by all developers, must be reviewed before they 70 | > are committed to the repository. Code reviews are conducted on GitHub through 71 | > comments on pull requests or commits. The developer responsible for a code 72 | > change is also responsible for making all necessary review-related changes. 73 | 74 | Sometimes code reviews will take longer than you would hope for, especially 75 | for larger features. Here are some accepted ways to speed up review times for 76 | your patches: 77 | 78 | - Review other people’s changes. If you help out, everybody will be more 79 | willing to do the same for you. Goodwill is our currency. 80 | - Split your change into multiple smaller changes. The smaller your change, 81 | the higher the probability that somebody will take a quick look at it. 82 | - Remember that you’re asking for 83 | valuable time from other professional developers. 84 | 85 | **Note that anyone is welcome to review and give feedback on a change, but 86 | only people with commit access to the repository can approve it.** 87 | 88 | ## Attribution of Changes 89 | 90 | When contributors submit a change to this project, after that change is 91 | approved, other developers with commit access may commit it for the author. 92 | When doing so, it is important to retain correct attribution of the 93 | contribution. Generally speaking, Git handles attribution automatically. 94 | 95 | ## Code Documentation 96 | 97 | Ensure that every function in this project is documented and follows the 98 | standards set by [JSDoc]. Finally, please stick to the code style as defined 99 | by the [Godaddy JS styleguide][style]. 100 | 101 | # Additional Resources 102 | 103 | - [General GitHub Documentation](https://help.github.com/) 104 | - [GitHub Pull Request documentation](https://help.github.com/send-pull-requests/) 105 | - [JSDoc] 106 | 107 | [issues]: https://github.com/warehouseai/cdnup/issues 108 | [JSDoc]: http://usejsdoc.org/ 109 | [npm]: http://npmjs.org/ 110 | [style]: https://github.com/godaddy/javascript/#godaddy-style 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `cdnup` 2 | 3 | > ⚠️ **DEPRECATED**: This package is no longer maintained and has been deprecated. Please use an alternative solution or contact the maintainers for more information. 4 | 5 | [![Version npm](https://img.shields.io/npm/v/cdnup.svg?style=flat-square)](https://www.npmjs.com/package/cdnup) 6 | [![License](https://img.shields.io/npm/l/cdnup.svg?style=flat-square)](https://github.com/warehouseai/cdnup/blob/master/LICENSE) 7 | [![npm Downloads](https://img.shields.io/npm/dm/cdnup.svg?style=flat-square)](https://npmcharts.com/compare/cdnup?minimal=true) 8 | [![Build Status](https://travis-ci.org/warehouseai/cdnup.svg?branch=master)](https://travis-ci.org/warehouseai/cdnup) 9 | [![Dependencies](https://img.shields.io/david/warehouseai/cdnup.svg?style=flat-square)](https://github.com/warehouseai/cdnup/blob/master/package.json) 10 | 11 | CDNup is a simple wrapper around `pkgcloud` which allows for a simple uploading 12 | interface as well as the ability to define a CDN URL that fronts whereever you 13 | are uploading your assets to. 14 | 15 | ## Installation 16 | 17 | ```sh 18 | npm install --save cdnup 19 | ``` 20 | 21 | ## Usage 22 | 23 | You can refer to [BFFS] to see `cdnup` in action. In all examples below we 24 | assume that you've already required and initialized the module as followed: 25 | 26 | ```js 27 | 'use strict'; 28 | 29 | const CDNUp = require('cdnup'); 30 | const cdnup = new CDNUp('bucket-name', { 31 | // 32 | // It is still assumed that the `bucket-name` prefix is appended to the 33 | // following url 34 | // 35 | url: 'https://myCdnEndpoint.com', 36 | pkgcloud: { /* Pkgcloud config options */ } 37 | }); 38 | ``` 39 | 40 | As you can see in the example above we allow 2 arguments in the constructor: 41 | 42 | 1. `bucket`: The relative path to the files on the CDN server. 43 | 2. `options`: Optional configuration object. The following keys are supported: 44 | - `sharding`: Randomly select one of the supplied `urls` of the CDN so assets 45 | can be sharded between different DNS/subdomains. 46 | - `url/urls`: A url string or urls array for what you will use to publicly 47 | fetch assets from the CDN. 48 | - `subdomain`: Boolean indicating the `bucket` should be used as subdomain. 49 | - `pkgcloud`: Options passed to `pkgcloud` constructor. 50 | - `mime`: Object containing custom mime types per file type. 51 | - `check`: Used to validate asset URL if the CDN assets are behind a firewall. 52 | 53 | ### Authorization 54 | 55 | We use [`pkgcloud`][pkgcloud] in order to upload CDN assets. It supports most if 56 | not all cloud providers depending on what you use and who you want to trust with 57 | your assets. Check out the documentation and our sample config to see how you 58 | may set this up for you. 59 | 60 | ```js 61 | const cdnup = new CDNUp('ux/core', { 62 | pkgcloud: { 63 | provider: 'amazon', // Use AWS s3 64 | forcePathBucket: // Inform AWS to use `s3ForcePathStyle` 65 | //... 66 | } 67 | }); 68 | ``` 69 | 70 | Note: more information about [`forcePathBucket` is available in AWS 71 | documentation][forcepath]. 72 | 73 | ## API 74 | 75 | The following API methods are available. 76 | 77 | ### upload 78 | 79 | This is the method that you will be using the most, `upload`. When you first 80 | call the method it might take a second to work because it will first create the 81 | bucket if that has not already been done 82 | 83 | Once initialized, it will write the files to the cloud provider and call your supplied 84 | callback. It requires 3 arguments: 85 | 86 | - A buffer, stream or path to the file that needs to be stored. 87 | - Filename of the thing that we're about to store. It will be `path.join`'ed 88 | with the `root` argument of the constructor. 89 | - Completion callback that follows the error first callback pattern. 90 | 91 | ```js 92 | cdnup.upload('/path/to/file.js', 'file.js', function (err) { 93 | if (err) return console.error('Shits on fire yo.'); 94 | 95 | console.log('all good'); 96 | }); 97 | ``` 98 | 99 | ### init 100 | 101 | Initialize the cloud provider with the given `bucket-name` passed to the 102 | constructor. 103 | 104 | ```js 105 | cdnup.init(function (err) { 106 | if (err) console.error('failed to mount cdn'); 107 | }); 108 | ``` 109 | 110 | ### url 111 | 112 | Return the URL and path of the CDN. 113 | 114 | ```js 115 | const fullCDNPath = cdnup.url(); 116 | ``` 117 | 118 | ### checkUrl 119 | 120 | Return the URL of the `file` specified against the configured `check`. 121 | 122 | ```js 123 | const cdn = new CDNUp('my-bucket', { 124 | check: 'https://my-bucket.s3.amazonaws.com/', 125 | url: 'https://whatever.com/world' 126 | }); 127 | 128 | // Will be rewritten against the specific `check`. 129 | const fileURL = cdn.checkUrl('https://whatever.com/world/hello-fixture.js'); 130 | ``` 131 | 132 | ### Test 133 | 134 | Run AWS local, pull `latest` [localstack]. 135 | This requires `docker` [to be setup][docker]. 136 | 137 | ```sh 138 | docker pull localstack/localstack:latest 139 | npm run localstack 140 | ``` 141 | 142 | Finally, run the unit test. 143 | 144 | ```bash 145 | npm test 146 | ``` 147 | 148 | [pkgcloud]: https://github.com/pkgcloud/pkgcloud 149 | [forcepath]: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#s3ForcePathStyle-property 150 | [BFFS]: https://github.com/warehouseai/bffs/blob/84354709fc0dc909341d72fed1466b46b130f655/index.js#L105-L118 151 | [localstack]: https://github.com/localstack/localstack 152 | --------------------------------------------------------------------------------