Update logging

This commit is contained in:
Bret Comnes 2020-02-12 16:52:32 -07:00
parent e49d0b8d3a
commit 383cb4ca53
No known key found for this signature in database
GPG Key ID: 3705F4634DC3A1AC
36 changed files with 824 additions and 597 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ sandbox.js
.nyc_output
config.json
public
node_modules

View File

@ -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)
}
}
}
}

View File

@ -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 }

View File

@ -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<String>} 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'
// }]

View File

@ -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')
})

2
node_modules/@types/node/README.md generated vendored
View File

@ -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`

View File

@ -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.

View File

@ -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"
}

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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.

View File

@ -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.
```
<center><img src="logo.jpg"></center>
```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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

312
node_modules/async-neocities/index.js generated vendored
View File

@ -1,18 +1,41 @@
const { handleResponse } = require('fetch-errors')
const { createReadStream } = require('fs')
const afw = require('async-folder-walker')
const FormData = require('form-data')
const assert = require('nanoassert')
const fetch = require('node-fetch')
const { URL } = require('url')
const qs = require('qs')
const os = require('os')
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('./lib/folder-diff')
const pkg = require('./package.json')
const SimpleTimer = require('./lib/timer')
const { getStreamLength, meterStream } = require('./lib/stream-meter')
const defaultURL = 'https://neocities.org'
// Progress API constants
const START = 'start'
const PROGRESS = 'progress' // progress updates
const STOP = 'stop'
// Progress stages
const INSPECTING = 'inspecting'
const DIFFING = 'diffing'
const APPLYING = 'applying'
/**
* NeocitiesAPIClient class representing a neocities api client.
*/
class NeocitiesAPIClient {
/**
* getKey returns an apiKey from a sitename and password.
* @param {String} sitename username/sitename to log into.
* @param {String} password password to log in with.
* @param {Object} [opts] Options object.
* @param {Object} [opts.url=https://neocities.org] Base URL to request to.
* @return {Promise<String>} 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

View File

@ -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>} 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',

View File

@ -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')

View File

@ -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"
}

48
node_modules/async-neocities/test.js generated vendored
View File

@ -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 })
})
}

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

3
node_modules/pump/package.json generated vendored
View File

@ -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",

11
node_modules/semver/package.json generated vendored
View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

3
node_modules/which/package.json generated vendored
View File

@ -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",

View File

@ -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": [

81
test.js
View File

@ -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)
})