1 | 'use strict'
|
2 |
|
3 | const { dirname } = require('path')
|
4 | const { mkdir, rm, stat } = require('fs').promises
|
5 | const { createHash } = require('crypto')
|
6 | const { createWriteStream } = require('fs')
|
7 | const http = require('http')
|
8 | const https = require('https')
|
9 | const { unlink } = require('fs/promises')
|
10 |
|
11 | const recursive = { recursive: true }
|
12 |
|
13 | const stripUrlHash = url => url.split('#')[0]
|
14 |
|
15 | const filename = url => {
|
16 | const hash = createHash('shake256', {
|
17 | outputLength: 8
|
18 | })
|
19 | hash.update(stripUrlHash(url))
|
20 | return hash.digest('base64')
|
21 | .replace(/=/g, '')
|
22 | .replace(/\+/g, '_')
|
23 | .replace(/\//g, '$')
|
24 | }
|
25 |
|
26 | const cleanDir = async dir => {
|
27 | try {
|
28 | await stat(dir)
|
29 | await rm(dir, recursive)
|
30 | } catch (err) {
|
31 |
|
32 | }
|
33 | }
|
34 |
|
35 | const $op = Symbol('pad.op')
|
36 | const $x = Symbol('pad.x')
|
37 | const $lt = Symbol('pad.lt')
|
38 | const $w = Symbol('pad.w')
|
39 | function pad (width) {
|
40 | if (!width) {
|
41 | width = process.stdout.columns || 80
|
42 | }
|
43 | const ops = {
|
44 | [$x] (widthLeft) {
|
45 | return ''.padStart(widthLeft, this.text)
|
46 | },
|
47 | [$lt] (widthLeft) {
|
48 | const { text, padding } = this
|
49 | if (text.length <= widthLeft) {
|
50 | return text.padEnd(widthLeft, padding)
|
51 | }
|
52 | return '...' + text.substring(text.length - widthLeft + 3)
|
53 | },
|
54 | [$w] (widthLeft, result, opIndex) {
|
55 | const { text } = this
|
56 | if (text.length < widthLeft && !text.includes('\n')) {
|
57 | return text.padEnd(widthLeft, ' ')
|
58 | }
|
59 | const lines = []
|
60 | text.split(/\r?\n/).forEach(line => {
|
61 | if (line.length <= widthLeft) {
|
62 | lines.push(line.padEnd(widthLeft, ' '))
|
63 | } else {
|
64 | for (let offset = 0; offset < line.length; offset += widthLeft - 1) {
|
65 | const part = line.slice(offset, offset + widthLeft - 1)
|
66 | if (part.length < widthLeft - 1) {
|
67 | lines.push(part.padEnd(widthLeft, ' '))
|
68 | } else {
|
69 | lines.push(`${part}↵`)
|
70 | }
|
71 | }
|
72 | }
|
73 | })
|
74 | const before = result.slice(0, opIndex).join('')
|
75 | const after = result.slice(opIndex + 1).join('')
|
76 | return lines.join(after + '\n' + before)
|
77 | }
|
78 | }
|
79 | return (strings, ...values) => {
|
80 | const result = []
|
81 | let op
|
82 | let opIndex
|
83 | const length = strings.reduce((total, string, index) => {
|
84 | result.push(string)
|
85 | total += string.length
|
86 | let value = values[index]
|
87 | if (value === null || value === undefined) {
|
88 | return total
|
89 | }
|
90 | if (value[$op]) {
|
91 | if (opIndex !== undefined) {
|
92 | throw new Error('Only one operator is allowed')
|
93 | }
|
94 | op = value
|
95 | opIndex = result.length
|
96 | result.push(value)
|
97 | } else {
|
98 | if (typeof value !== 'string') {
|
99 | value = value.toString()
|
100 | }
|
101 | result.push(value)
|
102 | total += value.length
|
103 | }
|
104 | return total
|
105 | }, 0)
|
106 | if (op !== undefined) {
|
107 | const widthLeft = width - length
|
108 | result[opIndex] = ops[op[$op]].call(op, widthLeft, result, opIndex)
|
109 | }
|
110 | return result.join('')
|
111 | }
|
112 | }
|
113 |
|
114 | pad.x = (text) => ({ [$op]: $x, text })
|
115 | pad.lt = (text, padding = ' ') => ({ [$op]: $lt, text, padding })
|
116 | pad.w = (text) => ({ [$op]: $w, text })
|
117 |
|
118 | function allocPromise () {
|
119 | let resolve
|
120 | let reject
|
121 | const promise = new Promise((_resolve, _reject) => {
|
122 | resolve = _resolve
|
123 | reject = _reject
|
124 | })
|
125 | return { promise, resolve, reject }
|
126 | }
|
127 |
|
128 | async function download (url, filename) {
|
129 | const { hostname, port, origin } = new URL(url)
|
130 | const options = {
|
131 | hostname,
|
132 | port,
|
133 | path: url.substring(origin.length),
|
134 | method: 'GET'
|
135 | }
|
136 | const protocol = url.startsWith('https:') ? https : http
|
137 | await mkdir(dirname(filename), recursive)
|
138 | const output = createWriteStream(filename)
|
139 | const { promise, resolve, reject } = allocPromise()
|
140 | const request = protocol.request(options, async response => {
|
141 | if (response.statusCode !== 200) {
|
142 | reject(response.statusCode)
|
143 | output.end()
|
144 | await unlink(filename)
|
145 | return
|
146 | }
|
147 | response.on('error', reject)
|
148 | response.on('end', resolve)
|
149 | response.pipe(output)
|
150 | })
|
151 | request.on('error', reject)
|
152 | request.end()
|
153 | return promise
|
154 | }
|
155 |
|
156 | module.exports = {
|
157 | stripUrlHash,
|
158 | filename,
|
159 | cleanDir,
|
160 | createDir: dir => mkdir(dir, recursive),
|
161 | recreateDir: dir => cleanDir(dir).then(() => mkdir(dir, recursive)),
|
162 | extractPageUrl: headers => headers['x-page-url'],
|
163 | allocPromise,
|
164 | noop () {},
|
165 | pad,
|
166 | download
|
167 | }
|