From 383cb4ca53c8d312aa671511dcbfaa36aa800a54 Mon Sep 17 00:00:00 2001 From: Bret Comnes Date: Wed, 12 Feb 2020 16:52:32 -0700 Subject: [PATCH] Update logging --- .gitignore | 1 + index.js | 91 ++++- lib/client.js | 172 ---------- lib/folder-diff.js | 159 --------- lib/folder-diff.test.js | 65 ---- node_modules/@types/node/README.md | 2 +- node_modules/@types/node/globals.d.ts | 6 + node_modules/@types/node/package.json | 12 +- .../.github/workflows/tests.yml | 25 -- node_modules/async-neocities/CHANGELOG.md | 76 ++++- .../async-neocities/CODE_OF_CONDUCT.md | 2 +- node_modules/async-neocities/CONTRIBUTING.md | 16 +- node_modules/async-neocities/README.md | 216 +++++++++++- node_modules/async-neocities/fixtures/cat.png | Bin 16793 -> 0 bytes .../async-neocities/fixtures/neocities.png | Bin 13232 -> 0 bytes .../async-neocities/fixtures/toot.gif | Bin 461 -> 0 bytes .../async-neocities/fixtures/tootzzz.png | Bin 1706 -> 0 bytes node_modules/async-neocities/index.js | 312 ++++++++++++++++-- .../async-neocities/lib/folder-diff.js | 40 ++- .../async-neocities/lib/folder-diff.test.js | 4 +- node_modules/async-neocities/package.json | 37 ++- node_modules/async-neocities/test.js | 48 +++ node_modules/end-of-stream/package.json | 1 + node_modules/is-stream/package.json | 4 +- node_modules/nanoassert/package.json | 3 +- node_modules/node-fetch/package.json | 3 +- node_modules/npm-run-path/package.json | 3 +- node_modules/p-finally/package.json | 3 +- node_modules/pump/package.json | 3 +- node_modules/semver/package.json | 11 +- node_modules/shebang-command/package.json | 3 +- node_modules/signal-exit/package.json | 3 +- node_modules/strip-eof/package.json | 3 +- node_modules/which/package.json | 3 +- package.json | 13 +- test.js | 81 +---- 36 files changed, 824 insertions(+), 597 deletions(-) delete mode 100644 lib/client.js delete mode 100644 lib/folder-diff.js delete mode 100644 lib/folder-diff.test.js delete mode 100644 node_modules/async-neocities/.github/workflows/tests.yml delete mode 100644 node_modules/async-neocities/fixtures/cat.png delete mode 100644 node_modules/async-neocities/fixtures/neocities.png delete mode 100755 node_modules/async-neocities/fixtures/toot.gif delete mode 100755 node_modules/async-neocities/fixtures/tootzzz.png diff --git a/.gitignore b/.gitignore index e73e090..f8d1919 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ sandbox.js .nyc_output config.json public +node_modules diff --git a/index.js b/index.js index 596bda4..323c83b 100644 --- a/index.js +++ b/index.js @@ -2,25 +2,100 @@ const core = require('@actions/core') // const github = require('@actions/github') const Neocities = require('async-neocities') const path = require('path') -const exec = require('child_process').exec +const prettyTime = require('pretty-time') +const prettyBytes = require('pretty-bytes') async function doDeploy () { const token = core.getInput('api-token') const distDir = path.join(process.cwd(), core.getInput('dist-dir')) const cleanup = core.getInput('cleanup') - const time = (new Date()).toTimeString() - core.setOutput('time', time) - const client = new Neocities(token) - return client.deploy(distDir, { + const finalStats = await client.deploy(distDir, { cleanup, - statusCb: console.log + statsCb: statsHandler({ cleanup, distDir }) }) + + return finalStats } -doDeploy().then(() => {}).catch(err => { - console.error(err) +doDeploy().then((finalStats) => {}).catch(err => { core.setFailed(err.message) }) + +function statsHandler (opts = {}) { + return (stats) => { + switch (stats.stage) { + case 'inspecting': { + switch (stats.status) { + case 'start': { + core.startGroup('Inspecting') + console.log(`Inspecting local (${opts.distDir}) and remote files...`) + break + } + case 'progress': { + break + } + case 'stop': { + console.log(`Done inspecting local and remote files in ${prettyTime([0, stats.timer.elapsed])}`) + const { tasks: { localScan, remoteScan } } = stats + console.log(`Scanned ${localScan.numberOfFiles} local files (${prettyBytes(localScan.totalSize)}) in ${prettyTime([0, localScan.timer.elapsed])}`) + console.log(`Scanned ${remoteScan.numberOfFiles} remote files (${prettyBytes(remoteScan.totalSize)}) in ${prettyTime([0, remoteScan.timer.elapsed])}`) + core.endGroup() + break + } + } + break + } + case 'diffing': { + switch (stats.status) { + case 'start': { + core.startGroup('Diffing files') + console.log('Diffing local and remote files...') + break + } + case 'progress': { + // No progress on diffing + break + } + case 'stop': { + const { tasks: { diffing } } = stats + console.log(`Done diffing local and remote files in ${prettyTime([0, stats.timer.elapsed])}`) + console.log(`${diffing.uploadCount} files to upload`) + console.log(`${diffing.deleteCount} ` + opts.cleanup ? 'files to delete' : 'orphaned files') + console.log(`${diffing.skipCoount} files to skip`) + core.endGroup() + break + } + } + break + } + case 'applying': { + switch (stats.status) { + case 'start': { + core.startGroup('Applying diff') + console.log('Uploading changes' + opts.cleanup ? ' and deleting orphaned files...' : '...') + break + } + case 'progress': { + break + } + case 'stop': { + const { tasks: { uploadFiles, deleteFiles, skippedFiles } } = stats + console.log('Done uploading changes' + opts.cleanup ? ' and deleting orphaned files' : '' + ` in ${prettyTime([0, stats.timer.elapsed])}`) + console.log(`Average upload speed: ${prettyBytes(uploadFiles.speed)}/s`) + if (opts.cleanup) console.log(`Average delete speed: ${prettyBytes(deleteFiles.speed)}/s`) + console.log(`Skipped ${skippedFiles.count} files (${prettyBytes(skippedFiles.size)})`) + core.endGroup() + break + } + } + break + } + default: { + console.log(stats) + } + } + } +} diff --git a/lib/client.js b/lib/client.js deleted file mode 100644 index 5764d5d..0000000 --- a/lib/client.js +++ /dev/null @@ -1,172 +0,0 @@ -const assert = require('nanoassert') -const fetch = require('node-fetch') -const { URL } = require('url') -const qs = require('qs') -const os = require('os') -const path = require('path') -const { createReadStream } = require('fs') -const FormData = require('form-data') -const { handleResponse } = require('fetch-errors') -const afw = require('async-folder-walker') -const pkg = require('../package.json') -const { neocitiesLocalDiff } = require('./folder-diff') - -const defaultURL = 'https://neocities.org' - -class NeocitiesAPIClient { - static getKey (sitename, password, opts) { - assert(sitename, 'must pass sitename as first arg') - assert(typeof sitename === 'string', 'user arg must be a string') - assert(password, 'must pass a password as the second arg') - assert(typeof password, 'password arg must be a string') - - opts = Object.assign({ - baseURL: defaultURL - }, opts) - - const baseURL = opts.baseURL - delete opts.baseURL - - const url = new URL('/api/key', baseURL) - url.username = sitename - url.password = password - return fetch(url, opts) - } - - constructor (apiKey, opts) { - assert(apiKey, 'must pass apiKey as first argument') - assert(typeof apiKey === 'string', 'apiKey must be a string') - opts = Object.assign({ - url: defaultURL - }) - - this.opts = opts - this.url = opts.url - this.apiKey = apiKey - } - - get defaultHeaders () { - return { - Authorization: `Bearer ${this.apiKey}`, - Accept: 'application/json', - 'User-Agent': `deploy-to-neocities/${pkg.version} (${os.type()})` - } - } - - /** - * Generic get request to neocities - */ - get (endpoint, quieries, opts) { - assert(endpoint, 'must pass endpoint as first argument') - opts = Object.assign({ - method: 'GET' - }, opts) - opts.headers = Object.assign({}, this.defaultHeaders, opts.headers) - - let path = `/api/${endpoint}` - if (quieries) path += `?${qs.stringify(quieries)}` - - const url = new URL(path, this.url) - return fetch(url, opts) - } - - /** - * Generic post request to neocities - */ - post (endpoint, formEntries, opts) { - assert(endpoint, 'must pass endpoint as first argument') - const form = new FormData() - opts = Object.assign({ - method: 'POST', - body: form - }, opts) - - for (const { name, value } of formEntries) { - form.append(name, value) - } - - opts.headers = Object.assign( - {}, - this.defaultHeaders, - form.getHeaders(), - opts.headers) - - const url = new URL(`/api/${endpoint}`, this.url) - return fetch(url, opts) - } - - /** - * Upload files to neocities - */ - upload (files) { - const formEntries = files.map(({ name, path }) => { - return { - name, - value: createReadStream(path) - } - }) - - return this.post('upload', formEntries).then(handleResponse) - } - - /** - * delete files from your website - */ - delete (filenames) { - assert(filenames, 'filenames is a required first argument') - assert(Array.isArray(filenames), 'filenames argument must be an array of file paths in your website') - - const formEntries = filenames.map(file => ({ - name: 'filenames[]', - value: file - })) - - return this.post('delete', formEntries).then(handleResponse) - } - - list (queries) { - // args.path: Path to list - return this.get('list', queries).then(handleResponse) - } - - /** - * info returns info on your site, or optionally on a sitename querystrign - * @param {Object} args Querystring arguments to include (e.g. sitename) - * @return {Promise} Fetch request promise - */ - info (queries) { - // args.sitename: sitename to get info on - return this.get('info', queries).then(handleResponse) - } - - /** - * Deploy a folder to neocities, skipping already uploaded files and optionally cleaning orphaned files. - * @param {String} folder The path of the folder to deploy. - * @param {Object} opts Options object. - * @param {Boolean} opts.cleanup Boolean to delete orphaned files nor not. Defaults to false. - * @param {Boolean} opts.statsCb Get access to stat info before uploading is complete. - * @return {Promise} Promise containing stats about the deploy - */ - async deploy (folder, opts) { - opts = { - cleanup: false, // delete remote orphaned files - statsCb: () => {}, - ...opts - } - - const [localFiles, remoteFiles] = Promise.all([ - afw.allFiles(path.join(folder), { shaper: f => f }), - this.list() - ]) - - const { filesToUpload, filesToDelete, filesSkipped } = await neocitiesLocalDiff(remoteFiles, localFiles) - opts.statsCb({ filesToUpload, filesToDelete, filesSkipped }) - const work = [this.upload(filesToUpload)] - if (opts.cleanup) work.push(this.delete(filesToDelete)) - - await work - - return { filesToUpload, filesToDelete, filesSkipped } - } -} -module.exports = { NeocitiesAPIClient } diff --git a/lib/folder-diff.js b/lib/folder-diff.js deleted file mode 100644 index de3f7e8..0000000 --- a/lib/folder-diff.js +++ /dev/null @@ -1,159 +0,0 @@ -const crypto = require('crypto') -const util = require('util') -const fs = require('fs') -const ppump = util.promisify(require('pump')) - -/** - * neocitiesLocalDiff returns an array of files to delete and update and some useful stats. - */ -async function neocitiesLocalDiff (neocitiesFiles, localListing, opts = {}) { - opts = { - ...opts - } - - const localIndex = {} - const ncIndex = {} - - const neoCitiesFiltered = neocitiesFiles.filter(f => !f.is_directory) - neoCitiesFiltered.forEach(f => { ncIndex[f.path] = f }) // index - const ncFiles = new Set(neoCitiesFiltered.map(f => f.path)) // shape - - const localListingFiltered = localListing.filter(f => !f.stat.isDirectory()) // files only - localListingFiltered.forEach(f => { localIndex[f.relname] = f }) // index - // TODO: convert windows to unix paths - const localFiles = new Set(localListingFiltered.map(f => f.relname)) // shape - - const filesToAdd = difference(localFiles, ncFiles) - const filesToDelete = difference(ncFiles, localFiles) - - const maybeUpdate = intersection(localFiles, ncFiles) - const skipped = new Set() - - for (const p of maybeUpdate) { - const local = localIndex[p] - const remote = ncIndex[p] - - if (local.stat.size !== remote.size) { filesToAdd.add(p); continue } - - const localSha1 = await sha1FromPath(local.filepath) - if (localSha1 !== remote.sha1_hash) { filesToAdd.add(p); continue } - - skipped.add(p) - } - - return { - filesToUpload: Array.from(filesToAdd).map(p => ({ - name: localIndex[p].relname, - path: localIndex[p].filepath - })), - filesToDelete: Array.from(filesToDelete).map(p => ncIndex[p].path), - filesSkipped: Array.from(skipped).map(p => localIndex[p]) - } -} - -module.exports = { - neocitiesLocalDiff -} - -/** - * sha1FromPath returns a sha1 hex from a path - * @param {String} p string of the path of the file to hash - * @return {Promise} the hex representation of the sha1 - */ -async function sha1FromPath (p) { - const rs = fs.createReadStream(p) - const hash = crypto.createHash('sha1') - - await ppump(rs, hash) - - return hash.digest('hex') -} - -// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set#Implementing_basic_set_operations - -/** - * difference betwen setA and setB - * @param {Set} setA LHS set - * @param {Set} setB RHS set - * @return {Set} The difference Set - */ -function difference (setA, setB) { - const _difference = new Set(setA) - for (const elem of setB) { - _difference.delete(elem) - } - return _difference -} - -function intersection (setA, setB) { - const _intersection = new Set() - for (const elem of setB) { - if (setA.has(elem)) { - _intersection.add(elem) - } - } - return _intersection -} - -// [ -// { -// path: 'img', -// is_directory: true, -// updated_at: 'Thu, 21 Nov 2019 04:06:17 -0000' -// }, -// { -// path: 'index.html', -// is_directory: false, -// size: 1094, -// updated_at: 'Mon, 11 Nov 2019 22:23:16 -0000', -// sha1_hash: '7f15617e87d83218223662340f4052d9bb9d096d' -// }, -// { -// path: 'neocities.png', -// is_directory: false, -// size: 13232, -// updated_at: 'Mon, 11 Nov 2019 22:23:16 -0000', -// sha1_hash: 'fd2ee41b1922a39a716cacb88c323d613b0955e4' -// }, -// { -// path: 'not_found.html', -// is_directory: false, -// size: 347, -// updated_at: 'Mon, 11 Nov 2019 22:23:16 -0000', -// sha1_hash: 'd7f004e9d3b2eaaa8827f741356f1122dc9eb030' -// }, -// { -// path: 'style.css', -// is_directory: false, -// size: 298, -// updated_at: 'Mon, 11 Nov 2019 22:23:16 -0000', -// sha1_hash: 'e516457acdb0d00710ab62cc257109ef67209ce8' -// } -// ] - -// [{ -// root: '/Users/bret/repos/async-folder-walker/fixtures', -// filepath: '/Users/bret/repos/async-folder-walker/fixtures/sub-folder/sub-sub-folder', -// stat: Stats { -// dev: 16777220, -// mode: 16877, -// nlink: 3, -// uid: 501, -// gid: 20, -// rdev: 0, -// blksize: 4096, -// ino: 30244023, -// size: 96, -// blocks: 0, -// atimeMs: 1574381262779.8396, -// mtimeMs: 1574380914743.5474, -// ctimeMs: 1574380914743.5474, -// birthtimeMs: 1574380905232.5996, -// atime: 2019-11-22T00:07:42.780Z, -// mtime: 2019-11-22T00:01:54.744Z, -// ctime: 2019-11-22T00:01:54.744Z, -// birthtime: 2019-11-22T00:01:45.233Z -// }, -// relname: 'sub-folder/sub-sub-folder', -// basename: 'sub-sub-folder' -// }] diff --git a/lib/folder-diff.test.js b/lib/folder-diff.test.js deleted file mode 100644 index a727943..0000000 --- a/lib/folder-diff.test.js +++ /dev/null @@ -1,65 +0,0 @@ -const tap = require('tap') -const afw = require('async-folder-walker') -const path = require('path') -const { neocitiesLocalDiff } = require('./folder-diff') - -const remoteFiles = [ - { - path: 'img', - is_directory: true, - updated_at: 'Thu, 21 Nov 2019 04:06:17 -0000' - }, - { - path: 'index.html', - is_directory: false, - size: 1094, - updated_at: 'Mon, 11 Nov 2019 22:23:16 -0000', - sha1_hash: '7f15617e87d83218223662340f4052d9bb9d096d' - }, - { - path: 'neocities.png', - is_directory: false, - size: 13232, - updated_at: 'Mon, 11 Nov 2019 22:23:16 -0000', - sha1_hash: 'fd2ee41b1922a39a716cacb88c323d613b0955e4' - }, - { - path: 'not_found.html', - is_directory: false, - size: 347, - updated_at: 'Mon, 11 Nov 2019 22:23:16 -0000', - sha1_hash: 'd7f004e9d3b2eaaa8827f741356f1122dc9eb030' - }, - { - path: 'style.css', - is_directory: false, - size: 298, - updated_at: 'Mon, 11 Nov 2019 22:23:16 -0000', - sha1_hash: 'e516457acdb0d00710ab62cc257109ef67209ce8' - } -] - -tap.test('test differ', async t => { - const localFiles = await afw.allFiles(path.join(__dirname, '../fixtures'), { - shaper: f => f - }) - - const { filesToUpload, filesToDelete, filesSkipped } = await neocitiesLocalDiff(remoteFiles, localFiles) - - t.true(['tootzzz.png', 'toot.gif', 'cat.png'].every(path => { - const found = filesToUpload.find(ftu => ftu.name === path) - t.ok(found.path && found.name, 'each file to upload has a name and path') - return found - }), 'every file to upload is included') - - t.deepEqual(filesToDelete, [ - 'index.html', - 'not_found.html', - 'style.css' - ], 'filesToDelete returned correctly') - - t.true(['neocities.png'].every(path => { - const found = filesSkipped.find(fs => fs.relname === path) - return found - }), 'every file skipped is included') -}) diff --git a/node_modules/@types/node/README.md b/node_modules/@types/node/README.md index f0697e6..7c8f134 100644 --- a/node_modules/@types/node/README.md +++ b/node_modules/@types/node/README.md @@ -8,7 +8,7 @@ This package contains type definitions for Node.js (http://nodejs.org/). Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node. ### Additional Details - * Last updated: Fri, 31 Jan 2020 21:34:20 GMT + * Last updated: Tue, 11 Feb 2020 17:16:28 GMT * Dependencies: none * Global values: `Buffer`, `Symbol`, `__dirname`, `__filename`, `clearImmediate`, `clearInterval`, `clearTimeout`, `console`, `exports`, `global`, `module`, `process`, `queueMicrotask`, `require`, `setImmediate`, `setInterval`, `setTimeout` diff --git a/node_modules/@types/node/globals.d.ts b/node_modules/@types/node/globals.d.ts index e5fcc20..61a5c5c 100644 --- a/node_modules/@types/node/globals.d.ts +++ b/node_modules/@types/node/globals.d.ts @@ -239,6 +239,12 @@ declare class Buffer extends Uint8Array { */ static from(data: number[]): Buffer; static from(data: Uint8Array): Buffer; + /** + * Creates a new buffer containing the coerced value of an object + * A `TypeError` will be thrown if {obj} has not mentioned methods or is not of other type appropriate for `Buffer.from()` variants. + * @param obj An object supporting `Symbol.toPrimitive` or `valueOf()`. + */ + static from(obj: { valueOf(): string | object } | { [Symbol.toPrimitive](hint: 'string'): string }, byteOffset?: number, length?: number): Buffer; /** * Creates a new Buffer containing the given JavaScript string {str}. * If provided, the {encoding} parameter identifies the character encoding. diff --git a/node_modules/@types/node/package.json b/node_modules/@types/node/package.json index 15cf02c..b707bbb 100644 --- a/node_modules/@types/node/package.json +++ b/node_modules/@types/node/package.json @@ -1,8 +1,8 @@ { "_from": "@types/node@>= 8", - "_id": "@types/node@13.7.0", + "_id": "@types/node@13.7.1", "_inBundle": false, - "_integrity": "sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ==", + "_integrity": "sha512-Zq8gcQGmn4txQEJeiXo/KiLpon8TzAl0kmKH4zdWctPj05nWwp1ClMdAVEloqrQKfaC48PNLdgN/aVaLqUrluA==", "_location": "/@types/node", "_phantomChildren": {}, "_requested": { @@ -20,8 +20,8 @@ "/@octokit/types", "/@types/glob" ], - "_resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.0.tgz", - "_shasum": "b417deda18cf8400f278733499ad5547ed1abec4", + "_resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.1.tgz", + "_shasum": "238eb34a66431b71d2aaddeaa7db166f25971a0d", "_spec": "@types/node@>= 8", "_where": "/Users/bret/repos/deploy-to-neocities/node_modules/@octokit/types", "bugs": { @@ -209,7 +209,7 @@ "scripts": {}, "typeScriptVersion": "2.8", "types": "index.d.ts", - "typesPublisherContentHash": "b0cd8dccd6d2eca8bb1de5a05bebf13796984567eb809f63164972122c4ddbaf", + "typesPublisherContentHash": "8b99aa0031fac941d520282519c47b0255109858a20251313b1210c28769f463", "typesVersions": { ">=3.5.0-0": { "*": [ @@ -217,5 +217,5 @@ ] } }, - "version": "13.7.0" + "version": "13.7.1" } diff --git a/node_modules/async-neocities/.github/workflows/tests.yml b/node_modules/async-neocities/.github/workflows/tests.yml deleted file mode 100644 index d7f6d5f..0000000 --- a/node_modules/async-neocities/.github/workflows/tests.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: tests - -on: [push] - -jobs: - test: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest] - node: [12] - - steps: - - uses: actions/checkout@v1 - - name: Use Node.js ${{ matrix.node }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node }} - - name: npm install && npm test - run: | - npm i - npm test - env: - CI: true diff --git a/node_modules/async-neocities/CHANGELOG.md b/node_modules/async-neocities/CHANGELOG.md index 9e1c638..457dd8e 100644 --- a/node_modules/async-neocities/CHANGELOG.md +++ b/node_modules/async-neocities/CHANGELOG.md @@ -1,8 +1,74 @@ -# async-neocities Change Log +# Changelog + All notable changes to this project will be documented in this file. -This project adheres to [Semantic Versioning](http://semver.org/). -## Unreleased +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## 0.0.1 - 2020-02-10 -* wip release +Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). + +## [v1.0.0](https://github.com/bcomnes/async-neocities/compare/v0.0.10...v1.0.0) - 2020-02-12 + +### Commits + +- feat: progress API sketch [`be8b9ec`](https://github.com/bcomnes/async-neocities/commit/be8b9ec062b5ea23157a6a841c9d66d03a85a8ca) +- docs: update README [`ec4f5f1`](https://github.com/bcomnes/async-neocities/commit/ec4f5f154b690dba0814ec0955fee674e8e94692) +- CHANGELOG [`c9b64ed`](https://github.com/bcomnes/async-neocities/commit/c9b64edd4d3db025adc737982477ce0d760f3254) + +## [v0.0.10](https://github.com/bcomnes/async-neocities/compare/v0.0.9...v0.0.10) - 2020-02-10 + +### Commits + +- dont do work unless there is work [`616a306`](https://github.com/bcomnes/async-neocities/commit/616a306ba3ca091da11c9c85bae2b07cb0b2768e) + +## [v0.0.9](https://github.com/bcomnes/async-neocities/compare/v0.0.8...v0.0.9) - 2020-02-10 + +### Commits + +- Use stream ctor [`e8201a0`](https://github.com/bcomnes/async-neocities/commit/e8201a053950848962a220b83ffa1a97ebab6e70) + +## [v0.0.8](https://github.com/bcomnes/async-neocities/compare/v0.0.7...v0.0.8) - 2020-02-10 + +### Commits + +- Fix more bugs [`95da7b7`](https://github.com/bcomnes/async-neocities/commit/95da7b7218082ab51c1463851f87428dc0c501ac) + +## [v0.0.7](https://github.com/bcomnes/async-neocities/compare/v0.0.6...v0.0.7) - 2020-02-10 + +### Commits + +- bugs [`71ead78`](https://github.com/bcomnes/async-neocities/commit/71ead78e0f48f619816b3ae3ea8154e8301c77ac) + +## [v0.0.6](https://github.com/bcomnes/async-neocities/compare/v0.0.5...v0.0.6) - 2020-02-10 + +### Commits + +- bugs [`c1d9973`](https://github.com/bcomnes/async-neocities/commit/c1d9973afef3abd7d6edfc5a6ae1c9d37f6cb34d) + +## [v0.0.5](https://github.com/bcomnes/async-neocities/compare/v0.0.4...v0.0.5) - 2020-02-10 + +### Commits + +- bugs [`e542111`](https://github.com/bcomnes/async-neocities/commit/e542111f3404ab923be3490e62ba16b4f6b66a70) + +## [v0.0.4](https://github.com/bcomnes/async-neocities/compare/v0.0.3...v0.0.4) - 2020-02-10 + +### Commits + +- bump version [`a3da5f7`](https://github.com/bcomnes/async-neocities/commit/a3da5f77cda15fb3e9ec5861b588f616d8b0055c) + +## [v0.0.3](https://github.com/bcomnes/async-neocities/compare/v0.0.2...v0.0.3) - 2020-02-10 + +### Commits + +- tmp releases [`16a6db4`](https://github.com/bcomnes/async-neocities/commit/16a6db49a06bebef89007b94e03dd34e6d17b298) + +## [v0.0.2](https://github.com/bcomnes/async-neocities/compare/v0.0.1...v0.0.2) - 2020-02-10 + +## v0.0.1 - 2020-02-10 + +### Commits + +- Init [`bb055ae`](https://github.com/bcomnes/async-neocities/commit/bb055ae8e76b0344acc929e8ffd3974d19144001) +- fix tests [`c294b52`](https://github.com/bcomnes/async-neocities/commit/c294b528a64a50638c4374a8782b177fe3634eb2) +- Init [`9ec8fb5`](https://github.com/bcomnes/async-neocities/commit/9ec8fb557ebf8578c9eb07dedffcb1b7eedbd3e6) diff --git a/node_modules/async-neocities/CODE_OF_CONDUCT.md b/node_modules/async-neocities/CODE_OF_CONDUCT.md index 237882d..9c93cac 100644 --- a/node_modules/async-neocities/CODE_OF_CONDUCT.md +++ b/node_modules/async-neocities/CODE_OF_CONDUCT.md @@ -1,4 +1,4 @@ # Code of conduct - This repo is governed as a dictatorship starting with the originator of the project. -- No malevolence tolerated whatsoever. +- This is a malevolence free zone. diff --git a/node_modules/async-neocities/CONTRIBUTING.md b/node_modules/async-neocities/CONTRIBUTING.md index b848425..e5ca73a 100644 --- a/node_modules/async-neocities/CONTRIBUTING.md +++ b/node_modules/async-neocities/CONTRIBUTING.md @@ -1,10 +1,22 @@ # Contributing +## Releasing + +Changelog, and releasing is autmated with npm scripts. To create a release: + +- Ensure a clean working git workspace. +- Run `npm version {patch,minor,major}`. + - This wills update the version number and generate the changelog. +- Run `npm publish`. + - This will push your local git branch and tags to the default remote, perform a [gh-release](https://ghub.io/gh-release), and create an npm publication. + +## Guidelines + - Patches, ideas and changes welcome. - Fixes almost always welcome. - Features sometimes welcome. - - Please open an issue to discuss the issue prior to spending lots of time on the problem. - - It may be rejected. + - Please open an issue to discuss the issue prior to spending lots of time on the problem. + - It may be rejected. - If you don't want to wait around for the discussion to commence, and you really want to jump into the implementation work, be prepared for fork if the idea is respectfully declined. - Try to stay within the style of the existing code. - All tests must pass. diff --git a/node_modules/async-neocities/README.md b/node_modules/async-neocities/README.md index 422e3f3..02188d8 100644 --- a/node_modules/async-neocities/README.md +++ b/node_modules/async-neocities/README.md @@ -1,9 +1,11 @@ # async-neocities [![Actions Status](https://github.com/bcomnes/async-neocities/workflows/tests/badge.svg)](https://github.com/bcomnes/async-neocities/actions) -WIP - nothing to see here +An api client for [neocities][nc] with an async/promise API and an efficient deploy algorithm. -``` +
+ +```console npm install async-neocities ``` @@ -28,6 +30,216 @@ deploySite.then(info => { console.log('done deploying site!') }) .catch(e => { throw e }) ``` +## API + +### `Neocities = require('async-neocities')` + +Import the Neocities API client. + +### `apiKey = await Neocities.getKey(sitename, password, [opts])` + +Static class method that will get an API Key from a sitename and password. + +`opts` include: + +```js +{ + url: 'https://neocities.org' // Base URL to use for requests +} +``` + +### `client = new Neocities(apiKey, [opts])` + +Create a new API client for a given API key. + +`opts` include: + +```js +{ + url: 'https://neocities.org' // Base URL to use for requests +} +``` + +### `response = await client.upload(files)` + +Pass an array of objects with the `{ name, path }` pair to upload these files to neocities, where `name` is desired remote unix path on neocities and `path` is the local path on disk in whichever format the local operating system desires. + +A successful `response`: + +```js +{ + result: 'success', + message: 'your file(s) have been successfully uploaded' +} +``` + +### `response = await client.delete(filenames)` + +Pass an array of path strings to delete on neocities. The path strings should be the unix style path of the file you want to delete. + +A successful `response`: + +```js +{ result: 'success', message: 'file(s) have been deleted' } +``` + +### `response = await client.list([queries])` + +Get a list of files for your site. The optional `queries` object is passed through [qs][qs] and added to the request. + +Available queries: + +```js +{ + path // list the contents of a subdirectory on neocities +} +``` + +Example `responses`: + +```json +{ + "result": "success", + "files": [ + { + "path": "index.html", + "is_directory": false, + "size": 1023, + "updated_at": "Sat, 13 Feb 2016 03:04:00 -0000", + "sha1_hash": "c8aac06f343c962a24a7eb111aad739ff48b7fb1" + }, + { + "path": "not_found.html", + "is_directory": false, + "size": 271, + "updated_at": "Sat, 13 Feb 2016 03:04:00 -0000", + "sha1_hash": "cfdf0bda2557c322be78302da23c32fec72ffc0b" + }, + { + "path": "images", + "is_directory": true, + "updated_at": "Sat, 13 Feb 2016 03:04:00 -0000" + }, + { + "path": "images/cat.png", + "is_directory": false, + "size": 16793, + "updated_at": "Sat, 13 Feb 2016 03:04:00 -0000", + "sha1_hash": "41fe08fc0dd44e79f799d03ece903e62be25dc7d" + } + ] +} +``` + +With the `path` query: + +```json +{ + "result": "success", + "files": [ + { + "path": "images/cat.png", + "is_directory": false, + "size": 16793, + "updated_at": "Sat, 13 Feb 2016 03:04:00 -0000", + "sha1_hash": "41fe08fc0dd44e79f799d03ece903e62be25dc7d" + } + ] +} +``` + +### `response = await client.info([queries])` + +Get info about your or other sites. The optional `queries` object is passed through [qs][qs] and added to the request. + +Available queries: + +```js +{ + sitename // get info on a given sitename +} +``` + +Example `responses`: + +```json +{ + "result": "success", + "info": { + "sitename": "youpi", + "hits": 5072, + "created_at": "Sat, 29 Jun 2013 10:11:38 +0000", + "last_updated": "Tue, 23 Jul 2013 20:04:03 +0000", + "domain": null, + "tags": [] + } +} +``` + +### `stats = await client.deploy(directory, [opts])` + +Deploy a path to a `directory`, efficiently only uploading missing and changed files. Files are determined to be different by size, and sha1 hash, if the size is the same. + +`opts` include: + +```js +{ + cleanup: false // delete orphaned files on neocities that are not in the `directory` + statsCb: () => {} // WIP progress API +} +``` + +The return value of this method is subject to change. + +### `client.get(endpoint, [quieries], [opts])` + +Low level GET request to a given `endpoint`. + +**NOTE**: The `/api/` prefix is automatically added: `/api/${endpoint}` so that must be omitted from `endpoint. + +The optional `queries` object is stringified to a querystring using [`qs`][qs]a and added to the request. + +`opts` includes: + +```js +{ + method: 'GET', + headers: { ...client.defaultHeaders, ...opts.headers }, +} +``` + +Note, that `opts` is passed internally to [`node-fetch`][nf] and you can include any options that work for that client here. + +### `client.post(endpoint, formEntries, [opts])` + +Low level POST request to a given `endpoint`. + +**NOTE**: The `/api/` prefix is automatically adeded: `/api/${endpoint}` so that must be omitted from `endpoint. + +Pass a `formEntries` array or iterator containing objects with `{name, value}` pairs to be sent with the POST request as [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData). The [form-datat][fd] module is used internally. + +`opts` include: + +```js +{ + method: 'POST', + body: new FormData(), // Don't override this. + headers: { ...client.defafultHeaders, ...formHeaders, opts.headers } +} +``` + +Note, that `opts` is passed internally to [`node-fetch`][nf] and you can include any options that work for that client here. + +## See also + +- [Neocities API docs](https://neocities.org/api) +- [Official Node.js API client](https://github.com/neocities/neocities-node) + ## License MIT + +[qs]: https://ghub.io/qs +[nf]: https://ghub.io/node-fetch +[fd]: https://ghub.io/form-data +[nc]: https://neocities.org diff --git a/node_modules/async-neocities/fixtures/cat.png b/node_modules/async-neocities/fixtures/cat.png deleted file mode 100644 index d4be8f2196d91a986c528a32cf918d588ea45eb1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16793 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
kp+VUhpk+a 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 diff --git a/node_modules/async-neocities/fixtures/toot.gif b/node_modules/async-neocities/fixtures/toot.gif deleted file mode 100755 index b75a98d6402ab0291c5da3feab8dfb403f550fd2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 461 zcmV;;0W$taNk%w1VHf}z0M$DH00030{{a8{0Qu4YjI8hf@&NwY0RQy>|Lp+R;QR3O z{{QI!|KkAv-T>aK0L|F?yvq3Ny#PjPihfF0O#)gVT|bi=KzGc^xEe8y3h7j zco6_?pD_*rfnabLMJ??EnA&0000000000 z00000A^8LW0027xEC2ui02lxn000K+z#U*mEE4lpz%Dg z*{$vaNr0W(n1=H(XO&vjjhs-NpW0WZ!cq8_O-ox6Eg3Knb^r!|MgTO7LJ~P76-xpF z1q%aMf?FaIO97&y2b!Fx0Hdk_2MiN;N(HN`0tlOd4F{v4qDut^4qHqB3buO00bT^(FO?Vy(k?WGb051{58W9 z4GX1^@s6$@ldn00001b5ch_0Itp) z=>Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn4jTXf20uweK~zXfwN-gg z)O8g9>;Wh&r)bI{9(W)aW#ToCT%w^6nTn`6%w*G;Sz$_PWoe4q=%g`@k}#XiByd+U znM`!|?p>;=sG#fDuZt?Xo*NkLt)gqRWce4CAS-6mX0k=;q0eDM75O>3kWbHEVnv<~ zBn0b92?+_LP$;a4|7-Ad8#fT-95gwaP;;D6e}-^tKdoshqe^NcnQAW~XB*-2k5psO zQ&DrH=&+))>bVstDJh}g;9%=LiI{<4Z8GDBE$LW#@d$8qJ-iz<7~{|bMLIRAC2c@$ zBcRt}sOm0y%UmFJ@j#u~0Ma+ZVf}XK3>qje7UR|6Cxc~Tp90BbQk0gKf&*ja%9RKS z3BmN~(>Z7dHB?qxN@B_(kKA2!sJuYbeA|4JkiV0LZB3epcluwpY#GJG#87y6I1L&!s7udzHr5N}PM$=#x}B19m7=nm^z=r_ zDe}+!j#QTfnWc}yD0L21*Ri@&^e4r~PNuxPeDPjxZZ4&#r&DHTrpQl|h+Nb3*n(Pn zj&NcHy7Rk-NIOLPI8eRP`HB{uus^mod}P z74c&uQE;UI*~%;o4lPAM;2?~fdkv60WUDH>b*U>Zhib!Z7-Wf}j^_ePmMnpnmlre| z4V;{u?2*q+OhhkV2{!xp0GbFRn#z$kbqXHznuNi~_mJ=00Ig)ZVrc^g;$_va&?E~Yt!kyI7ZUSthYzrw=ZbK z2vEpSQ1B4u!Jwf2pdmgW4oeO)(C}dcS?)(~&HIM-oUWsLT1%CinjVSBt-C!pzkuH^ z!PC|5pM3RsO=kJFWc!f^fg`rj0Yps`&r(4} An api key for the sitename.. + */ static getKey (sitename, password, opts) { assert(sitename, 'must pass sitename as first arg') assert(typeof sitename === 'string', 'user arg must be a string') @@ -20,11 +43,11 @@ class NeocitiesAPIClient { assert(typeof password, 'password arg must be a string') opts = Object.assign({ - baseURL: defaultURL + url: defaultURL }, opts) - const baseURL = opts.baseURL - delete opts.baseURL + const baseURL = opts.url + delete opts.url const url = new URL('/api/key', baseURL) url.username = sitename @@ -32,6 +55,13 @@ class NeocitiesAPIClient { return fetch(url, opts) } + /** + * Create an async-neocities api client. + * @param {string} apiKey An apiKey to make requests with. + * @param {Object} [opts] Options object. + * @param {Object} [opts.url=https://neocities.org] Base URL to make requests to. + * @return {Object} An api client instance. + */ constructor (apiKey, opts) { assert(apiKey, 'must pass apiKey as first argument') assert(typeof apiKey === 'string', 'apiKey must be a string') @@ -39,7 +69,6 @@ class NeocitiesAPIClient { url: defaultURL }) - this.opts = opts this.url = opts.url this.apiKey = apiKey } @@ -48,12 +77,18 @@ class NeocitiesAPIClient { return { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', - 'User-Agent': `deploy-to-neocities/${pkg.version} (${os.type()})` + 'User-Agent': `async-neocities/${pkg.version} (${os.type()})` } } /** - * Generic get request to neocities + * Generic GET request to neocities. + * @param {String} endpoint An endpoint path to GET request. + * @param {Object} [quieries] An object that gets added to the request in the form of a query string. + * @param {Object} [opts] Options object. + * @param {String} [opts.method=GET] The http method to use. + * @param {Object} [opts.headers] Headers to include in the request. + * @return {Object} The parsed JSON from the request response. */ get (endpoint, quieries, opts) { assert(endpoint, 'must pass endpoint as first argument') @@ -70,19 +105,46 @@ class NeocitiesAPIClient { } /** - * Generic post request to neocities + * Low level POST request to neocities with FormData. + * @param {String} endpoint The endpoint to make the request to. + * @param {Array.<{name: String, value: String}>} formEntries Array of form entries. + * @param {Object} [opts] Options object. + * @param {String} [opts.method=POST] HTTP Method. + * @param {Object} [opts.headers] Additional headers to send. + * @return {Object} The parsed JSON response object. */ - post (endpoint, formEntries, opts) { + async post (endpoint, formEntries, opts) { assert(endpoint, 'must pass endpoint as first argument') - const form = new FormData() + assert(formEntries, 'must pass formEntries as second argument') + + function createForm () { + const form = new FormData() + for (const { name, value } of formEntries) { + form.append(name, value) + } + + return form + } + opts = Object.assign({ method: 'POST', - body: form + statsCb: () => {} }, opts) + const statsCb = opts.statsCb + delete opts.statsCb - for (const { name, value } of formEntries) { - form.append(name, value) + const stats = { + totalBytes: await getStreamLength(createForm()), + bytesWritten: 0 } + statsCb(stats) + + const form = createForm() + + opts.body = meterStream(form, bytesRead => { + stats.bytesWritten = bytesRead + statsCb(stats) + }) opts.headers = Object.assign( {}, @@ -97,7 +159,11 @@ class NeocitiesAPIClient { /** * Upload files to neocities */ - upload (files) { + upload (files, opts = {}) { + opts = { + statsCb: () => {}, + ...opts + } const formEntries = files.map(({ name, path }) => { const streamCtor = (next) => next(createReadStream(path)) streamCtor.path = path @@ -107,22 +173,26 @@ class NeocitiesAPIClient { } }) - return this.post('upload', formEntries).then(handleResponse) + return this.post('upload', formEntries, { statsCb: opts.statsCb }).then(handleResponse) } /** * delete files from your website */ - delete (filenames) { + delete (filenames, opts = {}) { assert(filenames, 'filenames is a required first argument') assert(Array.isArray(filenames), 'filenames argument must be an array of file paths in your website') + opts = { + statsCb: () => {}, + ...opts + } const formEntries = filenames.map(file => ({ name: 'filenames[]', value: file })) - return this.post('delete', formEntries).then(handleResponse) + return this.post('delete', formEntries, { statsCb: opts.statsCb }).then(handleResponse) } list (queries) { @@ -141,34 +211,210 @@ class NeocitiesAPIClient { } /** - * Deploy a folder to neocities, skipping already uploaded files and optionally cleaning orphaned files. - * @param {String} folder The path of the folder to deploy. + * Deploy a directory to neocities, skipping already uploaded files and optionally cleaning orphaned files. + * @param {String} directory The path of the directory to deploy. * @param {Object} opts Options object. * @param {Boolean} opts.cleanup Boolean to delete orphaned files nor not. Defaults to false. * @param {Boolean} opts.statsCb Get access to stat info before uploading is complete. * @return {Promise} Promise containing stats about the deploy */ - async deploy (folder, opts) { + async deploy (directory, opts) { opts = { cleanup: false, // delete remote orphaned files statsCb: () => {}, ...opts } - const [localFiles, remoteFiles] = await Promise.all([ - afw.allFiles(folder, { shaper: f => f }), - this.list() - ]) + const statsCb = opts.statsCb + const startDeployTime = Date.now() + const totalTime = new SimpleTimer(startDeployTime) + + // Inspection stage stats initializer + const inspectionStats = { + stage: INSPECTING, + status: START, + timer: new SimpleTimer(startDeployTime), + totalTime, + tasks: { + localScan: { + numberOfFiles: 0, + totalSize: 0, + timer: new SimpleTimer(startDeployTime) + }, + remoteScan: { + numberOfFiles: 0, + totalSize: 0, + timer: new SimpleTimer(startDeployTime) + } + } + } + const sendInspectionUpdate = (status) => { + if (status) inspectionStats.status = status + statsCb(inspectionStats) + } + sendInspectionUpdate(START) + + // Remote scan timers + const remoteScanJob = this.list() + remoteScanJob.then(({ files }) => { // Comes in the form of a response object + const { tasks: { remoteScan } } = inspectionStats + remoteScan.numberOfFiles = files.length + remoteScan.totalSize = files.reduce((accum, cur) => { + return accum + cur.size || 0 + }, 0) + remoteScan.timer.stop() + sendInspectionUpdate(PROGRESS) + }) + + // Local scan timers and progress accumulator + const localScanJob = progressAccum( + afw.asyncFolderWalker(directory, { shaper: f => f }) + ) + async function progressAccum (iterator) { + const localFiles = [] + const { tasks: { localScan } } = inspectionStats + + for await (const file of iterator) { + localFiles.push(file) + localScan.numberOfFiles += 1 + localScan.totalSize += file.stat.blksize + sendInspectionUpdate(PROGRESS) + } + return localFiles + } + localScanJob.then(files => { + const { tasks: { localScan } } = inspectionStats + localScan.timer.stop() + sendInspectionUpdate(PROGRESS) + }) + + // Inspection stage finalizer + const [localFiles, remoteFiles] = await Promise.all([ + localScanJob, + this.list().then(res => res.files) + ]) + inspectionStats.timer.stop() + sendInspectionUpdate(STOP) + + // DIFFING STAGE + + const diffingStats = { + stage: DIFFING, + status: START, + timer: new SimpleTimer(Date.now()), + totalTime, + tasks: { + diffing: { + uploadCount: 0, + deleteCount: 0, + skipCount: 0 + } + } + } + statsCb(diffingStats) + + const { tasks: { diffing } } = diffingStats + const { filesToUpload, filesToDelete, filesSkipped } = await neocitiesLocalDiff(remoteFiles, localFiles) + diffingStats.timer.stop() + diffingStats.status = STOP + diffing.uploadCount = filesToUpload.length + diffing.deleteCount = filesToDelete.length + diffing.skipCount = filesSkipped.length + statsCb(diffingStats) + + const applyingStartTime = Date.now() + const applyingStats = { + stage: APPLYING, + status: START, + timer: new SimpleTimer(applyingStartTime), + totalTime, + tasks: { + uploadFiles: { + timer: new SimpleTimer(applyingStartTime), + bytesWritten: 0, + totalBytes: 0, + get percent () { + return this.totalBytes === 0 ? 0 : this.bytesWritten / this.totalBytes + }, + get speed () { + return this.bytesWritten / this.timer.elapsed + } + }, + deleteFiles: { + timer: new SimpleTimer(applyingStartTime), + bytesWritten: 0, + totalBytes: 0, + get percent () { + return this.totalBytes === 0 ? 0 : this.bytesWritten / this.totalBytes + }, + get speed () { + return this.bytesWritten / this.timer.elapsed + } + }, + skippedFiles: { + count: filesSkipped.length, + size: filesSkipped.reduce((accum, file) => accum + file.stat.blksize, 0) + } + } + } + const sendApplyingUpdate = (status) => { + if (status) applyingStats.status = status + statsCb(applyingStats) + } + sendApplyingUpdate(START) - const { filesToUpload, filesToDelete, filesSkipped } = await neocitiesLocalDiff(remoteFiles.files, localFiles) - opts.statsCb({ filesToUpload, filesToDelete, filesSkipped }) const work = [] - if (filesToUpload.length > 0) work.push(this.upload(filesToUpload)) - if (opts.cleanup && filesToDelete.length > 0) { work.push(this.delete(filesToDelete)) } + const { tasks: { uploadFiles, deleteFiles } } = applyingStats + + if (filesToUpload.length > 0) { + const uploadJob = this.upload(filesToUpload, { + statsCb: ({ bytesWritten, totalBytes }) => { + uploadFiles.bytesWritten = bytesWritten + uploadFiles.totalBytes = totalBytes + sendApplyingUpdate(PROGRESS) + } + }) + work.push(uploadJob) + uploadJob.then(res => { + uploadFiles.timer.stop() + sendApplyingUpdate(PROGRESS) + }) + } else { + uploadFiles.timer.stop() + } + + if (opts.cleanup && filesToDelete.length > 0) { + const deleteJob = this.delete(filesToDelete, { + statsCb: ({ bytesWritten, totalBytes }) => { + deleteFiles.bytesWritten = bytesWritten + deleteFiles.totalBytes = totalBytes + sendApplyingUpdate(PROGRESS) + } + }) + work.push(deleteJob) + deleteJob.then(res => { + deleteFiles.timer.stop() + sendApplyingUpdate(PROGRESS) + }) + } else { + deleteFiles.timer.stop() + } await Promise.all(work) + applyingStats.timer.stop() + sendApplyingUpdate(STOP) - return { filesToUpload, filesToDelete, filesSkipped } + totalTime.stop() + + const statsSummary = { + time: totalTime, + inspectionStats, + diffingStats, + applyingStats + } + + return statsSummary } } + module.exports = NeocitiesAPIClient diff --git a/node_modules/async-neocities/lib/folder-diff.js b/node_modules/async-neocities/lib/folder-diff.js index de3f7e8..62b3468 100644 --- a/node_modules/async-neocities/lib/folder-diff.js +++ b/node_modules/async-neocities/lib/folder-diff.js @@ -1,16 +1,16 @@ const crypto = require('crypto') const util = require('util') const fs = require('fs') + const ppump = util.promisify(require('pump')) /** * neocitiesLocalDiff returns an array of files to delete and update and some useful stats. + * @param {Array} neocitiesFiles Array of files returned from the neocities list api. + * @param {Array} localListing Array of files returned by a full data async-folder-walker run. + * @return {Promise} Object of filesToUpload, filesToDelete and filesSkipped. */ -async function neocitiesLocalDiff (neocitiesFiles, localListing, opts = {}) { - opts = { - ...opts - } - +async function neocitiesLocalDiff (neocitiesFiles, localListing) { const localIndex = {} const ncIndex = {} @@ -19,9 +19,8 @@ async function neocitiesLocalDiff (neocitiesFiles, localListing, opts = {}) { const ncFiles = new Set(neoCitiesFiltered.map(f => f.path)) // shape const localListingFiltered = localListing.filter(f => !f.stat.isDirectory()) // files only - localListingFiltered.forEach(f => { localIndex[f.relname] = f }) // index - // TODO: convert windows to unix paths - const localFiles = new Set(localListingFiltered.map(f => f.relname)) // shape + localListingFiltered.forEach(f => { localIndex[forceUnixRelname(f.relname)] = f }) // index + const localFiles = new Set(localListingFiltered.map(f => forceUnixRelname(f.relname))) // shape const filesToAdd = difference(localFiles, ncFiles) const filesToDelete = difference(ncFiles, localFiles) @@ -43,7 +42,7 @@ async function neocitiesLocalDiff (neocitiesFiles, localListing, opts = {}) { return { filesToUpload: Array.from(filesToAdd).map(p => ({ - name: localIndex[p].relname, + name: forceUnixRelname(localIndex[p].relname), path: localIndex[p].filepath })), filesToDelete: Array.from(filesToDelete).map(p => ncIndex[p].path), @@ -72,7 +71,7 @@ async function sha1FromPath (p) { // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set#Implementing_basic_set_operations /** - * difference betwen setA and setB + * difference returnss the difference betwen setA and setB. * @param {Set} setA LHS set * @param {Set} setB RHS set * @return {Set} The difference Set @@ -85,6 +84,12 @@ function difference (setA, setB) { return _difference } +/** + * intersection returns the interesction between setA and setB. + * @param {Set} setA setA LHS set + * @param {Set} setB setB RHS set + * @return {Set} The intersection set between setA and setB. + */ function intersection (setA, setB) { const _intersection = new Set() for (const elem of setB) { @@ -95,6 +100,18 @@ function intersection (setA, setB) { return _intersection } +/** + * forceUnixRelname forces a OS dependent path to a unix style path. + * @param {String} relname String path to convert to unix style. + * @return {String} The unix variant of the path + */ +function forceUnixRelname (relname) { + return relname.split(relname.sep).join('/') +} + +/** + * Example of neocitiesFiles + */ // [ // { // path: 'img', @@ -131,6 +148,9 @@ function intersection (setA, setB) { // } // ] +/** + * Example of localListing + */ // [{ // root: '/Users/bret/repos/async-folder-walker/fixtures', // filepath: '/Users/bret/repos/async-folder-walker/fixtures/sub-folder/sub-sub-folder', diff --git a/node_modules/async-neocities/lib/folder-diff.test.js b/node_modules/async-neocities/lib/folder-diff.test.js index a727943..02d2df8 100644 --- a/node_modules/async-neocities/lib/folder-diff.test.js +++ b/node_modules/async-neocities/lib/folder-diff.test.js @@ -1,6 +1,7 @@ -const tap = require('tap') const afw = require('async-folder-walker') const path = require('path') +const tap = require('tap') + const { neocitiesLocalDiff } = require('./folder-diff') const remoteFiles = [ @@ -53,7 +54,6 @@ tap.test('test differ', async t => { }), 'every file to upload is included') t.deepEqual(filesToDelete, [ - 'index.html', 'not_found.html', 'style.css' ], 'filesToDelete returned correctly') diff --git a/node_modules/async-neocities/package.json b/node_modules/async-neocities/package.json index b242f7b..b41a6b6 100644 --- a/node_modules/async-neocities/package.json +++ b/node_modules/async-neocities/package.json @@ -1,26 +1,26 @@ { - "_from": "async-neocities@0.0.10", - "_id": "async-neocities@0.0.10", + "_from": "async-neocities@1.0.0", + "_id": "async-neocities@1.0.0", "_inBundle": false, - "_integrity": "sha512-K6QNpBPNlZtRX7wGkL3f+i1HNJ5NNnq0dURGbDVYtrwwYN+PUfbqElugIsvyj+OIsO2wp5A7NBo1Wm6T3prSeg==", + "_integrity": "sha512-iRdvlFfyyqS390fGzs/FJOFG5izOJFVG/0w/xRoqZ6ochmjkxiByp16zjBb1Ade5lvXuKTuBdM/sdqmIQvWe5w==", "_location": "/async-neocities", "_phantomChildren": {}, "_requested": { "type": "version", "registry": true, - "raw": "async-neocities@0.0.10", + "raw": "async-neocities@1.0.0", "name": "async-neocities", "escapedName": "async-neocities", - "rawSpec": "0.0.10", + "rawSpec": "1.0.0", "saveSpec": null, - "fetchSpec": "0.0.10" + "fetchSpec": "1.0.0" }, "_requiredBy": [ "/" ], - "_resolved": "https://registry.npmjs.org/async-neocities/-/async-neocities-0.0.10.tgz", - "_shasum": "fcb1e7e49c1577a2f2735256e51ca9e51893b792", - "_spec": "async-neocities@0.0.10", + "_resolved": "https://registry.npmjs.org/async-neocities/-/async-neocities-1.0.0.tgz", + "_shasum": "cdb2d2c4f3a431ab2aba7982693f8922f94d4360", + "_spec": "async-neocities@1.0.0", "_where": "/Users/bret/repos/deploy-to-neocities", "author": { "name": "Bret Comnes", @@ -38,18 +38,27 @@ "nanoassert": "^2.0.0", "node-fetch": "^2.6.0", "pump": "^3.0.0", - "qs": "^6.9.1" + "pumpify": "^2.0.1", + "qs": "^6.9.1", + "streamx": "^2.6.0" }, "deprecated": false, "description": "WIP - nothing to see here", "devDependencies": { + "auto-changelog": "^1.16.2", "dependency-check": "^4.1.0", + "gh-release": "^3.5.0", "npm-run-all": "^4.1.5", "standard": "^13.1.0", "tap": "^14.10.2" }, "homepage": "https://github.com/bcomnes/async-neocities", - "keywords": [], + "keywords": [ + "neocities", + "async", + "api client", + "static hosting" + ], "license": "MIT", "main": "index.js", "name": "async-neocities", @@ -58,15 +67,17 @@ "url": "git+https://github.com/bcomnes/async-neocities.git" }, "scripts": { + "prepublishOnly": "git push --follow-tags && gh-release", "test": "run-s test:*", "test:deps": "dependency-check . --no-dev --no-peer", "test:standard": "standard", - "test:tape": "tap" + "test:tape": "tap", + "version": "auto-changelog -p --template keepachangelog auto-changelog --breaking-pattern 'BREAKING:' && git add CHANGELOG.md" }, "standard": { "ignore": [ "dist" ] }, - "version": "0.0.10" + "version": "1.0.0" } diff --git a/node_modules/async-neocities/test.js b/node_modules/async-neocities/test.js index f3c0b3a..e79d021 100644 --- a/node_modules/async-neocities/test.js +++ b/node_modules/async-neocities/test.js @@ -83,4 +83,52 @@ if (!fakeToken) { // console.log(deleteResults) t.equal(deleteResults.result, 'success', 'list result successfull') }) + + tap.test('can deploy folders', async t => { + const client = new NeocitiesAPIClient(token) + + // const statsCb = (stats) => { + // let logLine = `${stats.stage} ${stats.status} ${stats.timer.elapsed}` + // Object.entries(stats.tasks).forEach(([key, val]) => { + // logLine += ` ${key}: ${JSON.stringify(val)}` + // }) + // console.log(logLine) + // } + + const deployStats = await client.deploy( + resolve(__dirname, 'fixtures'), + { + // statsCb, + cleanup: false + } + ) + + t.ok(deployStats) + + // console.dir(deployStats, { depth: 99, colors: true }) + + const redeployStats = await client.deploy( + resolve(__dirname, 'fixtures'), + { + // statsCb, + cleanup: false + } + ) + + t.ok(redeployStats) + + // console.dir(redeployStats, { depth: 99, colors: true }) + + const cleanupStats = await client.deploy( + resolve(__dirname, 'fixtures/empty'), + { + // statsCb, + cleanup: true + } + ) + + t.ok(cleanupStats) + + // console.dir(cleanupStats, { depth: 99, colors: true }) + }) } diff --git a/node_modules/end-of-stream/package.json b/node_modules/end-of-stream/package.json index 4758f7d..d81a5f4 100644 --- a/node_modules/end-of-stream/package.json +++ b/node_modules/end-of-stream/package.json @@ -16,6 +16,7 @@ "fetchSpec": "^1.1.0" }, "_requiredBy": [ + "/duplexify", "/pump" ], "_resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", diff --git a/node_modules/is-stream/package.json b/node_modules/is-stream/package.json index a890ea9..61b2b10 100644 --- a/node_modules/is-stream/package.json +++ b/node_modules/is-stream/package.json @@ -17,7 +17,9 @@ }, "_requiredBy": [ "/execa", - "/hasha" + "/got", + "/hasha", + "/term-size/execa" ], "_resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "_shasum": "12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44", diff --git a/node_modules/nanoassert/package.json b/node_modules/nanoassert/package.json index e8366f8..759a650 100644 --- a/node_modules/nanoassert/package.json +++ b/node_modules/nanoassert/package.json @@ -16,7 +16,8 @@ "fetchSpec": "^2.0.0" }, "_requiredBy": [ - "/async-neocities" + "/async-neocities", + "/streamx" ], "_resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz", "_shasum": "a05f86de6c7a51618038a620f88878ed1e490c09", diff --git a/node_modules/node-fetch/package.json b/node_modules/node-fetch/package.json index 0b6a836..f8045e4 100644 --- a/node_modules/node-fetch/package.json +++ b/node_modules/node-fetch/package.json @@ -17,7 +17,8 @@ }, "_requiredBy": [ "/@octokit/request", - "/async-neocities" + "/async-neocities", + "/auto-changelog" ], "_resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", "_shasum": "e633456386d4aa55863f676a7ab0daa8fdecb0fd", diff --git a/node_modules/npm-run-path/package.json b/node_modules/npm-run-path/package.json index f1f5934..e280ea5 100644 --- a/node_modules/npm-run-path/package.json +++ b/node_modules/npm-run-path/package.json @@ -16,7 +16,8 @@ "fetchSpec": "^2.0.0" }, "_requiredBy": [ - "/execa" + "/execa", + "/term-size/execa" ], "_resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "_shasum": "35a9232dfa35d7067b4cb2ddf2357b1871536c5f", diff --git a/node_modules/p-finally/package.json b/node_modules/p-finally/package.json index 6bdc25a..1a5b973 100644 --- a/node_modules/p-finally/package.json +++ b/node_modules/p-finally/package.json @@ -16,7 +16,8 @@ "fetchSpec": "^1.0.0" }, "_requiredBy": [ - "/execa" + "/execa", + "/term-size/execa" ], "_resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "_shasum": "3fbcfb15b899a44123b34b6dcc18b724336a2cae", diff --git a/node_modules/pump/package.json b/node_modules/pump/package.json index 91f1480..c426215 100644 --- a/node_modules/pump/package.json +++ b/node_modules/pump/package.json @@ -17,7 +17,8 @@ }, "_requiredBy": [ "/async-neocities", - "/get-stream" + "/get-stream", + "/pumpify" ], "_resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "_shasum": "b4a2116815bde2f4e1ea602354e8c75565107a64", diff --git a/node_modules/semver/package.json b/node_modules/semver/package.json index d3a8fb9..3be5ccb 100644 --- a/node_modules/semver/package.json +++ b/node_modules/semver/package.json @@ -16,9 +16,16 @@ "fetchSpec": "^5.5.0" }, "_requiredBy": [ + "/caching-transform/make-dir", + "/cp-file/make-dir", "/cross-spawn", - "/make-dir", - "/normalize-package-data" + "/find-cache-dir/make-dir", + "/istanbul-lib-report/make-dir", + "/istanbul-lib-source-maps/make-dir", + "/normalize-package-data", + "/nyc/make-dir", + "/package-json", + "/semver-diff" ], "_resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "_shasum": "a954f931aeba508d307bbf069eff0c01c96116f7", diff --git a/node_modules/shebang-command/package.json b/node_modules/shebang-command/package.json index bfad8e7..34f8936 100644 --- a/node_modules/shebang-command/package.json +++ b/node_modules/shebang-command/package.json @@ -16,7 +16,8 @@ "fetchSpec": "^1.2.0" }, "_requiredBy": [ - "/cross-spawn" + "/cross-spawn", + "/term-size/cross-spawn" ], "_resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "_shasum": "44aac65b695b03398968c39f363fee5deafdf1ea", diff --git a/node_modules/signal-exit/package.json b/node_modules/signal-exit/package.json index ddabe65..da5e417 100644 --- a/node_modules/signal-exit/package.json +++ b/node_modules/signal-exit/package.json @@ -16,12 +16,13 @@ "fetchSpec": "^3.0.0" }, "_requiredBy": [ - "/caching-transform/write-file-atomic", "/execa", "/foreground-child", + "/gauge", "/nyc", "/restore-cursor", "/spawn-wrap", + "/term-size/execa", "/write-file-atomic" ], "_resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", diff --git a/node_modules/strip-eof/package.json b/node_modules/strip-eof/package.json index d67904e..1d2efd6 100644 --- a/node_modules/strip-eof/package.json +++ b/node_modules/strip-eof/package.json @@ -16,7 +16,8 @@ "fetchSpec": "^1.0.0" }, "_requiredBy": [ - "/execa" + "/execa", + "/term-size/execa" ], "_resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "_shasum": "bb43ff5598a6eb05d89b59fcd129c983313606bf", diff --git a/node_modules/which/package.json b/node_modules/which/package.json index aeaea8e..7d2330a 100644 --- a/node_modules/which/package.json +++ b/node_modules/which/package.json @@ -18,7 +18,8 @@ "_requiredBy": [ "/cross-spawn", "/foreground-child/cross-spawn", - "/spawn-wrap" + "/spawn-wrap", + "/term-size/cross-spawn" ], "_resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "_shasum": "a45043d54f5805316da8d62f9f50918d3da70b0a", diff --git a/package.json b/package.json index bb229dd..0695123 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,11 @@ "test-skip:deps": "dependency-check . --no-dev --no-peer", "test:standard": "standard", "test:tape": "tap", + "release": "git push --follow-tags && gh-release", + "version": "run-s version:*", + "version:1-changelog": "auto-changelog -p --template keepachangelog auto-changelog --breaking-pattern 'BREAKING:' && git add CHANGELOG.md", + "version:2-cleandeps": "rm -rf node_modules && npm i --only=prod && git add node_modules --force", + "postversion": "npm i", "clean": "rimraf dist && mkdirp dist", "build": "mkdir public && cp package.json public/test.json" }, @@ -27,12 +32,16 @@ "dependency-check": "^4.1.0", "npm-run-all": "^4.1.5", "standard": "^14.3.1", - "tap": "^14.10.5" + "gh-release": "^3.5.0", + "tap": "^14.10.5", + "auto-changelog": "^1.16.2" }, "dependencies": { "@actions/core": "1.2.2", "@actions/github": "2.1.0", - "async-neocities": "0.0.10" + "async-neocities": "1.0.0", + "pretty-bytes": "^5.3.0", + "pretty-time": "^1.1.0" }, "standard": { "ignore": [ diff --git a/test.js b/test.js index bd4030b..5d4183a 100644 --- a/test.js +++ b/test.js @@ -1,82 +1,5 @@ const tap = require('tap') -const { readFileSync } = require('fs') -const { resolve } = require('path') -const { NeocitiesAPIClient } = require('./lib/client') - -let token = process.env.NEOCITIES_API_TOKEN - -if (!token) { - try { - const config = JSON.parse(readFileSync(resolve(__dirname, 'config.json'))) - token = config.token - } catch (e) { - console.warn('error loading config.json') - console.warn(e) - } -} - -tap.test('token loaded', async t => { - t.ok(token) -}) - -tap.test('basic client api', async t => { - const client = new NeocitiesAPIClient(token) - - t.ok(client.info, 'info method available') - t.ok(client.list, 'list method available') - t.ok(client.get, 'get method available') - t.ok(client.post, 'post method available') -}) - -tap.test('can get info about site', async t => { - const client = new NeocitiesAPIClient(token) - - const info = await client.info() - // console.log(info) - t.equal(info.result, 'success', 'info requesst successfull') - const list = await client.list() - // console.log(list) - t.equal(list.result, 'success', 'list result successfull') -}) - -// test('form data works the way I think', t => { -// const form = new FormData(); -// const p = resolve(__dirname, 'package.json'); -// form.append('package.json', next => next(createReadStream(p))); -// -// const concatStream = concat((data) => { -// console.log(data); -// t.end(); -// }); -// -// form.on('error', (err) => { -// t.error(err); -// }); -// form.pipe(concatStream); -// }); - -tap.test('can upload and delete files', async t => { - const client = new NeocitiesAPIClient(token) - - const uploadResults = await client.upload([ - { - name: 'toot.gif', - path: resolve(__dirname, 'fixtures/toot.gif') - }, - { - name: 'img/tootzzz.png', - path: resolve(__dirname, 'fixtures/tootzzz.png') - } - ]) - - // console.log(uploadResults) - t.equal(uploadResults.result, 'success', 'list result successfull') - - const deleteResults = await client.delete([ - 'toot.gif', - 'img/tootzzz.png' - ]) - // console.log(deleteResults) - t.equal(deleteResults.result, 'success', 'list result successfull') +tap.test('test', async t => { + t.ok(true) })