├── .gitignore ├── Colab_gd_utils.ipynb ├── Colab_gd_utils_Test专用版.ipynb ├── aria2.js ├── backup-db.js ├── backup └── .keep ├── bookmark.js ├── changelog.md ├── check.js ├── clear-db.js ├── compare.md ├── config.js ├── copy ├── count ├── create-table.sql ├── db.js ├── dedupe ├── doc ├── bot-worked.png └── tgbot-appache2-note.md ├── gdurl.sqlite ├── package-lock.json ├── package.json ├── readme.md ├── sa ├── .keep └── invalid │ └── .keep ├── server.js ├── src ├── gd.js ├── router.js ├── summary.js ├── tg.js └── tree.js ├── static ├── autorclone.png ├── choose.png ├── colab.png ├── count.png ├── error-log.png ├── gclone.png ├── gdurl.png ├── tree.min.css ├── tree.min.js └── tree.png └── validate-sa.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | gdurl.sqlite* 3 | config.js 4 | sa/*.json 5 | backup/*.sqlite 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /Colab_gd_utils.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "nbformat": 4, 3 | "nbformat_minor": 0, 4 | "metadata": { 5 | "colab": { 6 | "name": "Colab-gd-utils.ipynb", 7 | "provenance": [], 8 | "include_colab_link": true 9 | }, 10 | "kernelspec": { 11 | "name": "python3", 12 | "display_name": "Python 3" 13 | } 14 | }, 15 | "cells": [ 16 | { 17 | "cell_type": "markdown", 18 | "metadata": { 19 | "id": "view-in-github", 20 | "colab_type": "text" 21 | }, 22 | "source": [ 23 | "\"Open" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": { 29 | "id": "3Z8cK8h2Avbv", 30 | "colab_type": "text" 31 | }, 32 | "source": [ 33 | "### 防止Google Colab自动断开代码:\n", 34 | "

每60分钟自动运行代码刷新,解除90分钟断开限制.\n", 35 | "

使用方法:colab页面按下 F12或者 Ctrl+Shift+I (mac按 Option+Command+I) 在console(控制台) 输入以下代码并回车.

复制以下代码粘贴在浏览器console!!不要关闭浏览器以免失效\n", 36 | "\n", 37 | "\n", 38 | "```javascript\n", 39 | "function ConnectButton(){\n", 40 | " console.log(\"Connect pushed\"); \n", 41 | " document.querySelector(\"#connect\").click() \n", 42 | "}\n", 43 | "setInterval(ConnectButton,60000);\n", 44 | "```" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "metadata": { 50 | "id": "jtClEMAMLVHw", 51 | "colab_type": "code", 52 | "cellView": "form", 53 | "colab": {} 54 | }, 55 | "source": [ 56 | "#@markdown

← 输入了代码后运行以防止断开\n", 57 | "\n", 58 | "\n", 59 | "import IPython\n", 60 | "from google.colab import output\n", 61 | "\n", 62 | "display(IPython.display.Javascript('''\n", 63 | " function ClickConnect(){\n", 64 | " btn = document.querySelector(\"colab-connect-button\")\n", 65 | " if (btn != null){\n", 66 | " console.log(\"Click colab-connect-button\"); \n", 67 | " btn.click() \n", 68 | " }\n", 69 | " \n", 70 | " btn = document.getElementById('ok')\n", 71 | " if (btn != null){\n", 72 | " console.log(\"Click reconnect\"); \n", 73 | " btn.click() \n", 74 | " }\n", 75 | " }\n", 76 | " \n", 77 | "setInterval(ClickConnect,60000)\n", 78 | "'''))\n", 79 | "\n", 80 | "print(\"Done.\")" 81 | ], 82 | "execution_count": null, 83 | "outputs": [] 84 | }, 85 | { 86 | "cell_type": "markdown", 87 | "metadata": { 88 | "colab_type": "text", 89 | "id": "ICXqpDfanEX-" 90 | }, 91 | "source": [ 92 | "### 第一次使用必读:\n", 93 | "

第一次使用请将SA的 json文件放在 sa 文件夹下,并将文件夹上传至你下面将要挂载的网盘的根目录。之后先运行页面最下方的打包sa文件夹命令,因此命令只需运行一次,故放在最下方\n", 94 | "\n", 95 | "\n", 96 | "(如果你使用过gclone. Autoclone, FolderClone可以在相应的account文件夹下发现他们,如果从未使用过,请根据原作者GitHub的介绍生成)\n", 97 | "\n", 98 | "##

本Colab基于iwestlin大佬的谷歌百宝箱制作而成,并作了相应修改使其适合在Colab上运行,原项目地址:[https://github.com/iwestlin/gd-utils](https://github.com/iwestlin/gd-utils).\n", 99 | "##

本项目仓库地址:[https://github.com/iErics/gd-utils](https://github.com/iErics/gd-utils)\n" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "metadata": { 105 | "id": "VoQCd8Mnpp9q", 106 | "colab_type": "code", 107 | "cellView": "form", 108 | "colab": {} 109 | }, 110 | "source": [ 111 | "#@markdown

← 运行只需点击此处,下同\n", 112 | "#@markdown
\"Gdrive-logo\"/\n", 113 | "#@markdown

挂载谷歌云盘,并安装gd-utils\n", 114 | "#@markdown

此处需要挂载的云盘只需存有sa.zip文件,不必与转存盘有关联\n", 115 | "from google.colab import drive\n", 116 | "from IPython.display import clear_output\n", 117 | "\n", 118 | "drive.mount('/content/drive')\n", 119 | "\n", 120 | "clear_output()\n", 121 | "print(\"开始安装gd-utils\")\n", 122 | "\n", 123 | "!curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -\n", 124 | "clear_output()\n", 125 | "!sudo apt-get install -y nodejs\n", 126 | "clear_output()\n", 127 | "!git clone https://github.com/iErics/gd-utils\n", 128 | "clear_output()\n", 129 | "%cd /content/gd-utils\n", 130 | "!npm install --unsafe-perm=true --allow-root\n", 131 | "\n", 132 | "clear_output()\n", 133 | "print(\"安装成功,开始配置sa文件夹\")\n", 134 | "\n", 135 | "%cp /content/drive/\"My Drive\"/sa.zip /content/gd-utils\n", 136 | "!unzip -o -q /content/gd-utils/sa.zip -d /content/gd-utils\n", 137 | "\n", 138 | "clear_output()\n", 139 | "print(\"配置完成\")\n", 140 | "\n" 141 | ], 142 | "execution_count": null, 143 | "outputs": [] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "metadata": { 148 | "id": "GIuVg9Q7yAta", 149 | "colab_type": "code", 150 | "cellView": "form", 151 | "colab": {} 152 | }, 153 | "source": [ 154 | "#@markdown

搬运区

\n", 155 | "source = \"\" #@param {type:\"string\"}\n", 156 | "destination = \"\" #@param {type:\"string\"}\n", 157 | "\n", 158 | "\n", 159 | "print(\"-------我要开动了,GKD-------\\n-------初音祈祷中....\\n\")\n", 160 | "\n", 161 | "#!multifolderclone -s \"$source\" -d \"$destination\"\n", 162 | "\n", 163 | "!node --max-old-space-size=4096 copy \"$source\" \"$destination\" -S\n", 164 | "\n", 165 | "\n", 166 | "print(\"\\n----------复制完成----------\\n\")" 167 | ], 168 | "execution_count": null, 169 | "outputs": [] 170 | }, 171 | { 172 | "cell_type": "code", 173 | "metadata": { 174 | "id": "uVWTQI9bZjaF", 175 | "colab_type": "code", 176 | "cellView": "form", 177 | "colab": {} 178 | }, 179 | "source": [ 180 | "#@markdown

统计区

\n", 181 | "#@markdown

Options 为附加命令,如常用的 -u 表示强制从线上获取信息(无视是否存在本地缓存),可不填。

\n", 182 | "FolderID = \"\" #@param {type:\"string\"}\n", 183 | "Options = \"\" #@param {type:\"string\"}\n", 184 | "\n", 185 | "\n", 186 | "\n", 187 | "\n", 188 | "\n", 189 | "print(\"-------我要开动了,GKD-------\\n-------初音祈祷中....\\n\")\n", 190 | "\n", 191 | "#!multifolderclone -s \"$source\" -d \"$destination\"\n", 192 | "\n", 193 | "!node --max-old-space-size=4096 count \"$FolderID\" \"$Options:\" -S\n", 194 | "\n", 195 | "\n", 196 | "print(\"\\n----------统计完成----------\\n\")" 197 | ], 198 | "execution_count": null, 199 | "outputs": [] 200 | }, 201 | { 202 | "cell_type": "code", 203 | "metadata": { 204 | "id": "CPEOJ9CdIlIV", 205 | "colab_type": "code", 206 | "cellView": "form", 207 | "colab": {} 208 | }, 209 | "source": [ 210 | "#@markdown

剔除不能使用的SA

\n", 211 | "#@markdown

请在下方填入一个你的可用SA能读取的目录ID,一般目标盘ID或其内的目录ID即可, 不要用分享ID

\n", 212 | "\n", 213 | "ID = \"\" #@param {type: \"string\"}\n", 214 | "\n", 215 | "!./validate-sa.js \"$ID\"" 216 | ], 217 | "execution_count": null, 218 | "outputs": [] 219 | }, 220 | { 221 | "cell_type": "code", 222 | "metadata": { 223 | "id": "dRaSe6ujk2RK", 224 | "colab_type": "code", 225 | "cellView": "form", 226 | "colab": {} 227 | }, 228 | "source": [ 229 | "#@markdown

查看复制命令帮助信息

\n", 230 | "!./copy -h" 231 | ], 232 | "execution_count": null, 233 | "outputs": [] 234 | }, 235 | { 236 | "cell_type": "code", 237 | "metadata": { 238 | "id": "xMtlQIu51GGT", 239 | "colab_type": "code", 240 | "cellView": "form", 241 | "colab": {} 242 | }, 243 | "source": [ 244 | "#@markdown

查看统计命令帮助信息

\n", 245 | "!./count -h" 246 | ], 247 | "execution_count": null, 248 | "outputs": [] 249 | }, 250 | { 251 | "cell_type": "code", 252 | "metadata": { 253 | "id": "lV2AFyDzReeE", 254 | "colab_type": "code", 255 | "cellView": "form", 256 | "colab": {} 257 | }, 258 | "source": [ 259 | "#@markdown

打包sa文件夹请看向这里

\n", 260 | "MODE = \"ZIP\"\n", 261 | "FILENAME = \"sa.zip\" \n", 262 | "PATH_TO_FILE = \"/content/drive/My Drive/sa\"\n", 263 | "ARCHIVE_PASSWORD = \"\"\n", 264 | "compress = 1\n", 265 | "from pathlib import PurePosixPath\n", 266 | "from IPython.display import clear_output\n", 267 | "from google.colab import drive\n", 268 | "drive.mount('/content/drive')\n", 269 | "\n", 270 | "pathList = PATH_TO_FILE.split('|')\n", 271 | "if MODE == \"ZIP\":\n", 272 | " if not FILENAME:\n", 273 | " FILENAME = \"/content/NEW_FILE.ZIP\"\n", 274 | " if ARCHIVE_PASSWORD:\n", 275 | " passADD = f'--password \"{ARCHIVE_PASSWORD}\"'\n", 276 | " else:\n", 277 | " passADD = ''\n", 278 | " for part in pathList:\n", 279 | " pathdic = PurePosixPath(part.strip())\n", 280 | " parent = pathdic.parent\n", 281 | " partName = pathdic.parts[-1]\n", 282 | " cmd = f'cd \"{parent}\" && zip {passADD} -{compress} -v -r -u \"{FILENAME}\" \"{partName}\"'\n", 283 | " !$cmd\n", 284 | "clear_output()\n", 285 | "print(\"\\n----------打包成功----------\\n\")" 286 | ], 287 | "execution_count": null, 288 | "outputs": [] 289 | } 290 | ] 291 | } -------------------------------------------------------------------------------- /Colab_gd_utils_Test专用版.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "nbformat": 4, 3 | "nbformat_minor": 0, 4 | "metadata": { 5 | "colab": { 6 | "name": "Colab-gd-utils.ipynb", 7 | "provenance": [], 8 | "include_colab_link": true 9 | }, 10 | "kernelspec": { 11 | "name": "python3", 12 | "display_name": "Python 3" 13 | } 14 | }, 15 | "cells": [ 16 | { 17 | "cell_type": "markdown", 18 | "metadata": { 19 | "id": "view-in-github", 20 | "colab_type": "text" 21 | }, 22 | "source": [ 23 | "\"Open" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": { 29 | "id": "3Z8cK8h2Avbv", 30 | "colab_type": "text" 31 | }, 32 | "source": [ 33 | "### 防止Google Colab自动断开代码:\n", 34 | "

每60分钟自动运行代码刷新,解除90分钟断开限制.\n", 35 | "

使用方法:colab页面按下 F12或者 Ctrl+Shift+I (mac按 Option+Command+I) 在console(控制台) 输入以下代码并回车.

复制以下代码粘贴在浏览器console!!不要关闭浏览器以免失效\n", 36 | "\n", 37 | "\n", 38 | "```javascript\n", 39 | "function ConnectButton(){\n", 40 | " console.log(\"Connect pushed\"); \n", 41 | " document.querySelector(\"#connect\").click() \n", 42 | "}\n", 43 | "setInterval(ConnectButton,60000);\n", 44 | "```" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "metadata": { 50 | "id": "jtClEMAMLVHw", 51 | "colab_type": "code", 52 | "cellView": "form", 53 | "colab": {} 54 | }, 55 | "source": [ 56 | "#@markdown

← 输入了代码后运行以防止断开\n", 57 | "\n", 58 | "\n", 59 | "import IPython\n", 60 | "from google.colab import output\n", 61 | "\n", 62 | "display(IPython.display.Javascript('''\n", 63 | " function ClickConnect(){\n", 64 | " btn = document.querySelector(\"colab-connect-button\")\n", 65 | " if (btn != null){\n", 66 | " console.log(\"Click colab-connect-button\"); \n", 67 | " btn.click() \n", 68 | " }\n", 69 | " \n", 70 | " btn = document.getElementById('ok')\n", 71 | " if (btn != null){\n", 72 | " console.log(\"Click reconnect\"); \n", 73 | " btn.click() \n", 74 | " }\n", 75 | " }\n", 76 | " \n", 77 | "setInterval(ClickConnect,60000)\n", 78 | "'''))\n", 79 | "\n", 80 | "print(\"Done.\")" 81 | ], 82 | "execution_count": null, 83 | "outputs": [] 84 | }, 85 | { 86 | "cell_type": "markdown", 87 | "metadata": { 88 | "colab_type": "text", 89 | "id": "ICXqpDfanEX-" 90 | }, 91 | "source": [ 92 | "### 第一次使用必读:\n", 93 | "\n", 94 | "

本Colab基于iwestlin大佬的谷歌百宝箱制作而成,并作了相应修改使其适合在Colab上运行,原项目地址:https://github.com/iwestlin/gd-utils.\n" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "metadata": { 100 | "id": "VoQCd8Mnpp9q", 101 | "colab_type": "code", 102 | "cellView": "form", 103 | "colab": {} 104 | }, 105 | "source": [ 106 | "#@markdown

← 运行只需点击此处,下同\n", 107 | "#@markdown
\"Gdrive-logo\"/\n", 108 | "#@markdown

挂载谷歌云盘,并安装gd-utils\n", 109 | "#@markdown

此处需要挂载的云盘只需存有sa.zip文件,不必与转存盘有关联\n", 110 | "from google.colab import drive\n", 111 | "from IPython.display import clear_output\n", 112 | "\n", 113 | "drive.mount('/content/drive')\n", 114 | "\n", 115 | "clear_output()\n", 116 | "print(\"开始安装gd-utils\")\n", 117 | "\n", 118 | "!curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -\n", 119 | "clear_output()\n", 120 | "!sudo apt-get install -y nodejs\n", 121 | "clear_output()\n", 122 | "!git clone https://github.com/iErics/gd-utils\n", 123 | "clear_output()\n", 124 | "%cd /content/gd-utils\n", 125 | "!npm install --unsafe-perm=true --allow-root\n", 126 | "\n", 127 | "clear_output()\n", 128 | "print(\"安装成功,开始配置sa文件夹\")\n", 129 | "\n", 130 | "%cp /content/drive/\"Shared drives\"/Test/sa.zip /content/gd-utils\n", 131 | "!unzip -o -q /content/gd-utils/sa.zip -d /content/gd-utils\n", 132 | "\n", 133 | "clear_output()\n", 134 | "print(\"配置完成\")\n", 135 | "\n" 136 | ], 137 | "execution_count": null, 138 | "outputs": [] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "metadata": { 143 | "id": "GIuVg9Q7yAta", 144 | "colab_type": "code", 145 | "cellView": "form", 146 | "colab": {} 147 | }, 148 | "source": [ 149 | "#@markdown

搬运区

\n", 150 | "source = \"\" #@param {type:\"string\"}\n", 151 | "destination = \"\" #@param {type:\"string\"}\n", 152 | "\n", 153 | "\n", 154 | "print(\"-------我要开动了,GKD-------\\n-------初音祈祷中....\\n\")\n", 155 | "\n", 156 | "#!multifolderclone -s \"$source\" -d \"$destination\"\n", 157 | "\n", 158 | "!node --max-old-space-size=4096 copy \"$source\" \"$destination\" -S\n", 159 | "\n", 160 | "\n", 161 | "print(\"\\n----------复制完成----------\\n\")" 162 | ], 163 | "execution_count": null, 164 | "outputs": [] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "metadata": { 169 | "id": "uVWTQI9bZjaF", 170 | "colab_type": "code", 171 | "cellView": "form", 172 | "colab": {} 173 | }, 174 | "source": [ 175 | "#@markdown

统计区

\n", 176 | "#@markdown

Options 为附加命令,如常用的 -u 表示强制从线上获取信息(无视是否存在本地缓存),可不填。

\n", 177 | "FolderID = \"\" #@param {type:\"string\"}\n", 178 | "Options = \"\" #@param {type:\"string\"}\n", 179 | "\n", 180 | "\n", 181 | "\n", 182 | "\n", 183 | "\n", 184 | "print(\"-------我要开动了,GKD-------\\n-------初音祈祷中....\\n\")\n", 185 | "\n", 186 | "#!multifolderclone -s \"$source\" -d \"$destination\"\n", 187 | "\n", 188 | "!node --max-old-space-size=4096 count \"$FolderID\" \"$Options:\" -S\n", 189 | "\n", 190 | "\n", 191 | "print(\"\\n----------统计完成----------\\n\")" 192 | ], 193 | "execution_count": null, 194 | "outputs": [] 195 | }, 196 | { 197 | "cell_type": "code", 198 | "metadata": { 199 | "id": "dRaSe6ujk2RK", 200 | "colab_type": "code", 201 | "cellView": "form", 202 | "colab": {} 203 | }, 204 | "source": [ 205 | "#@markdown

查看复制命令帮助信息

\n", 206 | "!./copy -h" 207 | ], 208 | "execution_count": null, 209 | "outputs": [] 210 | }, 211 | { 212 | "cell_type": "code", 213 | "metadata": { 214 | "id": "xMtlQIu51GGT", 215 | "colab_type": "code", 216 | "cellView": "form", 217 | "colab": {} 218 | }, 219 | "source": [ 220 | "#@markdown

查看统计命令帮助信息

\n", 221 | "!./count -h" 222 | ], 223 | "execution_count": null, 224 | "outputs": [] 225 | } 226 | ] 227 | } -------------------------------------------------------------------------------- /aria2.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs') 4 | const crypto = require('crypto') 5 | 6 | const { format_size } = require('./src/summary') 7 | const { get_name_by_id, get_sa_token, get_access_token, walk_and_save, validate_fid } = require('./src/gd') 8 | 9 | const ID_DIR_MAPPING = {} 10 | const FOLDER_TYPE = 'application/vnd.google-apps.folder' 11 | 12 | const { argv } = require('yargs') 13 | .usage('用法: ./$0 [options]') 14 | .alias('o', 'output') 15 | .describe('output', '指定输出文件,不填默认为uri.txt') 16 | .alias('u', 'update') 17 | .describe('u', '不使用本地缓存,强制从线上获取源文件夹信息') 18 | .alias('S', 'service_account') 19 | .describe('S', '使用service account进行操作,前提是必须在 ./sa 目录下放置sa授权json文件') 20 | .alias('k', 'hashkey') 21 | .describe('k', '使用 https://github.com/iwestlin/gdshare 部署的网站所设置的hashkey,用于生成合法的下载链接') 22 | .alias('c', 'cf') 23 | .describe('cf', '使用 gdshare 部署的网站网址') 24 | .alias('e', 'expire') 25 | .describe('e', 'gdshare 直链过期时间,单位小时,默认值24') 26 | .help('h') 27 | .alias('h', 'help') 28 | 29 | const [fid] = argv._ 30 | if (validate_fid(fid)) { 31 | let { update, service_account, output, hashkey, cf, expire } = argv 32 | output = output || 'uri.txt' 33 | gen_input_file({ fid, update, service_account, output, hashkey, cf, expire }) 34 | .then(cmd => { 35 | console.log('已生成', output) 36 | console.log('执行命令即可下载:\n', cmd) 37 | }) 38 | .catch(console.error) 39 | } else { 40 | console.warn('目录ID缺失或格式错误') 41 | } 42 | 43 | async function gen_input_file ({ fid, service_account, update, output, hashkey, cf, expire }) { 44 | const root = await get_name_by_id(fid, service_account) 45 | const data = await walk_and_save({ fid, service_account, update }) 46 | const files = data.filter(v => v.mimeType !== FOLDER_TYPE) 47 | const folders = data.filter(v => v.mimeType === FOLDER_TYPE) 48 | let result 49 | if (hashkey && cf) { 50 | result = [`# aria2c -c --enable-rpc=false -i ${output}`] 51 | } else { 52 | const access_token = service_account ? (await get_sa_token()).access_token : await get_access_token() 53 | result = [`# aria2c -c --enable-rpc=false --header "Authorization: Bearer ${access_token}" -i ${output}`] 54 | } 55 | result = result.concat(files.map(file => { 56 | const { id, name, parent, size } = file 57 | const dir = get_dir(parent, folders) 58 | const download_uri = (hashkey && cf) ? gen_direct_link({ file, hashkey, cf, expire }) : `https://www.googleapis.com/drive/v3/files/${id}?alt=media` 59 | return `# 文件大小:${format_size(size)} 60 | ${download_uri} 61 | dir=${root}${dir} 62 | out=${name}` 63 | })) 64 | fs.writeFileSync(output, result.join('\n\n')) 65 | return result[0].replace('# ', '') 66 | } 67 | 68 | function gen_direct_link ({ file, hashkey, cf, expire }) { 69 | const { name, id } = file 70 | const expired = Date.now() + (Number(expire) || 24) * 3600 * 1000 71 | const str = `expired=${expired}&id=${id}` 72 | const sig = hmac(str, hashkey) 73 | if (!cf.startsWith('http')) cf = 'https://' + cf 74 | return `${cf}/api/download/${name}?${str}&sig=${sig}` 75 | } 76 | 77 | function hmac (str, hashkey) { 78 | return crypto.createHmac('sha256', hashkey).update(str).digest('hex') 79 | } 80 | 81 | function get_dir (id, folders) { 82 | let result = ID_DIR_MAPPING[id] 83 | if (result !== undefined) return result 84 | result = '' 85 | let temp = id 86 | let folder = folders.filter(v => v.id === temp)[0] 87 | while (folder) { 88 | result = `/${folder.name}` + result 89 | temp = folder.parent 90 | if (ID_DIR_MAPPING[temp]) { 91 | result = ID_DIR_MAPPING[temp] + result 92 | return ID_DIR_MAPPING[id] = result 93 | } 94 | folder = folders.filter(v => v.id === temp)[0] 95 | } 96 | return ID_DIR_MAPPING[id] = result 97 | } 98 | -------------------------------------------------------------------------------- /backup-db.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path') 4 | const {db} = require('./db') 5 | 6 | const filepath = path.join(__dirname, 'backup', `${Date.now()}.sqlite`) 7 | 8 | db.backup(filepath) 9 | .then(() => { 10 | console.log(filepath) 11 | }) 12 | .catch((err) => { 13 | console.log('backup failed:', err) 14 | }) 15 | -------------------------------------------------------------------------------- /backup/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErics/gd-utils/f4c8450a3f81ba65c7208031d9fdfbb2eeb7980c/backup/.keep -------------------------------------------------------------------------------- /bookmark.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const {db} = require('./db') 3 | 4 | const action = process.argv[2] || 'export' 5 | const filepath = process.argv[3] || 'bookmarks.json' 6 | 7 | if (action === 'export') { 8 | const bookmarks = db.prepare('select * from bookmark').all() 9 | fs.writeFileSync(filepath, JSON.stringify(bookmarks)) 10 | console.log('bookmarks exported', filepath) 11 | } else if (action === 'import') { 12 | let bookmarks = fs.readFileSync(filepath, 'utf8') 13 | bookmarks = JSON.parse(bookmarks) 14 | bookmarks.forEach(v => { 15 | const {alias, target} = v 16 | const exist = db.prepare('select alias from bookmark where alias=?').get(alias) 17 | if (exist) { 18 | db.prepare('update bookmark set target=? where alias=?').run(target, alias) 19 | } else { 20 | db.prepare('INSERT INTO bookmark (alias, target) VALUES (?, ?)').run(alias, target) 21 | } 22 | }) 23 | console.log('bookmarks imported', bookmarks) 24 | } else { 25 | console.log('[help info]') 26 | console.log('export: node bookmark.js export bm.json') 27 | console.log('import: node bookmark.js import bm.json') 28 | } 29 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ## 更新日志 2 | > 更新方法:在 gd-utils 目录下,执行 `git pull` 拉取最新代码,如果你使用了 pm2 守护进程,执行`pm2 reload server`刷新生效。 3 | 4 | ### [2020-08-06] 5 | - 由于最近`userRateLimitExceeded`的错误越来越频繁出现,看上去Google除了每日750G以外又加上了什么限制。我只好把剔除SA的条件从“连续2次”遇到这种报错消息改成了**连续7次**……这个值也可以自定义,只需要在`config.js`中导出一个 `EXCEED_LIMIT`的变量,具体方法请参考[专家设置](https://github.com/iwestlin/gd-utils/blob/master/readme.md#%E4%B8%93%E5%AE%B6%E8%AE%BE%E7%BD%AE) 6 | 7 | - 另外为了用户体验还隐去了包含`rate limit`的报错信息,同时将重置SA(也就是重新启用被剔除的SA)改成了每2小时执行一次(原来是12小时) 8 | 9 | ### [2020-08-05] 10 | - 配合[gdshare](https://github.com/iwestlin/gdshare)使用,给 [aria2.js](./aria2.js) 添加 `--hashkey` `--cf` `--expire` 选项,具体含义请执行 `./aria2.js -h` 查看。 11 | 使用示例: 12 | ```bash 13 | ./aria2.js folderID -k 'your hashkey' -c 'your.domain.com' -S 14 | ``` 15 | 这条命令会在当前目录生成一个包含所有文件下载链接的文本文件并输出一条以 `aria2c` 开头的命令,执行这条命令即可调用[aria2c](https://aria2.github.io/)下载整个`folderID`目录,并保留目录结构。 16 | 17 | ### [2020-08-02] 关于“没有可用的SA”报错信息 18 | - 最近Google Drive的 API 似乎有点抽风,转存新分享的资源时经常随机遇到`userRateLimitExceeded`(一般这种错误只会在SA用完每日转存流量750G时触发)的接口返回错误导致SA被剔除(即使是新加的SA也会遇到),而对于比较老的分享或者自己团队盘文件对拷则没问题。 19 | 不得已我只好修改了一下程序的逻辑,只有当SA连续两次遇到`userRateLimitExceeded`的错误时才会被剔除,在这种条件下,据我的观察,拷贝一个新分享的资源时,平均每转存100个文件会被剔除掉一个SA。 20 | 如果你不希望因为接口返回`userRateLimitExceeded`而剔除掉对应的SA,可以手动修改代码,方法见:https://github.com/iwestlin/gd-utils/issues/138#issuecomment-666156273 21 | - 命令行添加 `--dncf` 参数,表示`do not copy folders`,即转存资源时不复制任何递归子目录,直接将所有文件拷贝到新生成的文件夹中。 22 | 23 | [2020-07-28] 24 | - 添加 [aria2.js](https://github.com/iwestlin/gd-utils/blob/master/aria2.js) 脚本,方便利用 `aria2c` 下载google drive目录,使用帮助可执行 `./aria2.js -h` 查看。 25 | 26 | 相关 issue: [https://github.com/iwestlin/gd-utils/issues/133](https://github.com/iwestlin/gd-utils/issues/133) 27 | 使用录屏:[https://drive.google.com/file/d/1lzN7R9Klw66C5UttUUDN3_EsN3pNs62q/view](https://drive.google.com/file/d/1lzN7R9Klw66C5UttUUDN3_EsN3pNs62q/view) 28 | 29 | [2020-07-21] 30 | - 添加数据库clear脚本,只需在`gd-utils`目录下执行`node clear-db.js`就可以删除所有获取的文件信息(但会保留拷贝记录和bookmark)同时减小数据库文件大小了 31 | - 调整提取分享链接的方法,基本能够识别所有类型消息中的分享ID 32 | 33 | [2020-07-17] 34 | - 给命令行 `./dedupe` 添加 `--yes` (简写`-y`)选项,表示“若发现重复项,不询问直接删除” 35 | - 给 `./copy` `./count` `./dedupe` 添加 `--sa` 选项,可指定sa目录位置 36 | 示例用法 `./count folderID -S --sa test`,表示读取 `./test` 目录下的sa json文件(而不是默认的./sa目录) 37 | 38 | [2020-07-15] 39 | - 给tg机器人添加「强制刷新」和「清除按钮」按钮。 40 | 点击「强制刷新」可以无视本地缓存强制从线上获取对应链接的文件信息,省去手动输入 `/count folderID -u` 的麻烦 41 | 点击「清除按钮」可以清除链接消息下的多个按钮,适合任务完成后清除,防止误触 42 | 43 | [2020-07-11] 44 | - 给tg机器人添加单文件链接(`https://drive.google.com/file/d/1gfR...`)转存功能 45 | 46 | [2020-07-10] 47 | - 添加树形导出类型,示例用法: `./count folder-id -S -t tree -o tree.html` 48 | 49 | [tree.html](https://gdurl.viegg.com/api/gdrive/count?fid=1A35MT6auEHASo3egpZ3VINMOwvA47cJG&type=tree)可直接用浏览器打开: 50 | ![](./static/tree.png) 51 | 52 | 前端源码:[https://github.com/iwestlin/foldertree/blob/master/app.jsx](https://github.com/iwestlin/foldertree/blob/master/app.jsx) 53 | 54 | [2020-07-08] 55 | - 添加[colab脚本](https://github.com/iwestlin/gd-utils/issues/50#issuecomment-655298073) 56 | 57 | [2020-07-07] 58 | - 在复制文件时不使用p-limit依赖,改为while循环控制并行请求数,从而大大减少复制十万及以上数量级文件时的内存占用,避免进程被node强行退出。 59 | - 给机器人添加更多 /task 功能,支持清除所有已完成任务、删除特定任务 60 | 61 | [2020-07-06] 62 | - 给机器人添加收藏功能,[使用示例](https://drive.google.com/drive/folders/1sW8blrDT8o7882VOpXXr3pzXR73d4yGX) 63 | 64 | [2020-07-05] 65 | - pm2 启动脚本换成 `pm2 start server.js --node-args="--max-old-space-size=4096"`,避免任务文件数超大时内存占用太高被node干掉。 66 | 67 | [2020-07-04]**【重要更新】** 68 | - 解决了长时间拷贝命令突然出现 `Invalid Credentials` 错误的问题。 69 | 原因是依赖的[gtoken](https://www.npmjs.com/package/gtoken)在过期时间后并不返回新的access_token...之前有不少朋友遇到过,一开始我还以为是sa同时使用太多触发了Google限制,直到我自己将sa分批使用量降到了50却也依然遇到了这种报错…… 70 | - 提升了拷贝大量文件时数据库的操作效率,大大减少了cpu占用。(由于更改了数据库的结构,所以如果有未完成的任务,请先跑完任务再更新。如果更新代码后再继续之前未完成的任务,会导致无法接上进度。) 71 | - 如果触发团队盘40万文件数限制,会返回明确的错误消息,而不是之前的 `创建目录失败,请检查您的账号是否有相关权限` 72 | - 如果创建目录未完成被中断,相同命令重新开始执行后,会保留原始目录的结构继续创建目录。(之前会导致结构被打乱) 73 | 74 | [2020-07-03] 75 | - 给命令行 ./copy 命令添加了 `-D` 选项,表示不在目的地创建同名文件夹,直接将源文件夹中的文件原样复制到目的文件夹中 76 | 77 | [2020-07-02] 78 | - 机器人 /task 命令返回的进度信息每 10 秒更新一次 79 | - `./dedupe` 改为将重复文件移动到回收站(需要内容管理者及以上权限) 80 | - 给 sqlite 打开 WAL 模式提升效率 81 | - 提前5分钟将access_token判定为过期,减少未授权错误 82 | 83 | [2020-07-01](建议所有使用tg机器人的用户更新) 84 | - 给机器人的 `/count` 和 `/copy` 命令添加了 `-u` 参数的支持,命令最后加上 -u 表示强制从线上获取源文件夹信息 85 | - 允许继续状态为已完成的任务(适合搭配 -u 参数,增量复制刚分享出来的未更新完毕的文件夹) 86 | - 支持识别转发的 [@gdurl](https://t.me/s/gdurl) 频道消息中的google drive链接 87 | - 机器人回复任务完成消息时,携带 `成功拷贝目录(文件)数/总目录(文件)数` 88 | - 机器人回复统计消息时,携带文件夹名称 89 | - 机器人回复`/task`消息时,携带源文件夹名称链接和新文件夹链接 90 | - 当统计表格太长导致机器人发送消息失败时,发送统计概要 91 | - 增加了 [专家设置](#专家设置) 一节,保障HTTPS接口安全 92 | 93 | [2020-06-30] 94 | - 命令行操作时,不换行输出进度信息,同时将进度信息输出间隔调整为1秒 95 | - 隐藏 timeout exceed 报错信息 96 | 97 | ## 重要更新(2020-06-29) 98 | 如果你遇到了以下几种问题,请务必阅读此节: 99 | 100 | - 任务异常中断 101 | - 命令行日志无限循环输出但进度不变 102 | - 复制完发现丢文件 103 | 104 | 有不少网友遇到这些问题,但是作者一直无法复现,直到有tg网友发了张运行日志截图: 105 | ![](./static/error-log.png) 106 | 报错日志的意思是找不到对应的目录ID,这种情况会发生在SA没有对应目录的阅读权限的时候。 107 | 当进行server side copy时,需要向Google的服务器提交要复制的文件ID,和复制的位置,也就是新创建的目录ID,由于在请求时是随机选取的SA,所以当选中没有权限的SA时,这次拷贝请求没有对应目录的权限,就会发生图中的错误。 108 | 109 | **所以,上述这些问题的源头是,sa目录下,混杂了没有权限的json文件!** 110 | 111 | 以下是解决办法: 112 | - 在项目目录下,执行 `git pull` 拉取最新代码 113 | - 执行 `./validate-sa.js -h` 查看使用说明 114 | - 选择一个你的sa拥有阅读权限的目录ID,执行 `./validate-sa.js 你的目录ID` 115 | 116 | 程序会读取sa目录下所有json文件,依次检查它们是否拥有对 `你的目录ID` 的阅读权限,如果最后发现了无效的SA,程序会提供选项允许用户将无效的sa json移动到特定目录。 117 | 118 | 将无效sa文件移动以后,如果你使用了pm2启动,需要 `pm2 reload server` 重启下进程。 119 | 120 | 操作示例: [https://drive.google.com/drive/folders/1iiTAzWF_v9fo_IxrrMYiRGQ7QuPrnxHf](https://drive.google.com/drive/folders/1iiTAzWF_v9fo_IxrrMYiRGQ7QuPrnxHf) 121 | -------------------------------------------------------------------------------- /check.js: -------------------------------------------------------------------------------- 1 | const { ls_folder } = require('./src/gd') 2 | 3 | ls_folder({ fid: 'root' }).then(console.log).catch(console.error) 4 | -------------------------------------------------------------------------------- /clear-db.js: -------------------------------------------------------------------------------- 1 | const { db } = require('./db') 2 | 3 | const record = db.prepare('select count(*) as c from gd').get() 4 | db.prepare('delete from gd').run() 5 | console.log('已删除', record.c, '条数据') 6 | 7 | db.exec('vacuum') 8 | db.close() 9 | -------------------------------------------------------------------------------- /compare.md: -------------------------------------------------------------------------------- 1 | # 对比本工具和其他类似工具在 server side copy 的速度上的差异 2 | 3 | 以拷贝[https://drive.google.com/drive/folders/1W9gf3ReGUboJUah-7XDg5jKXKl5XwQQ3](https://drive.google.com/drive/folders/1W9gf3ReGUboJUah-7XDg5jKXKl5XwQQ3)为例([文件统计](https://gdurl.viegg.com/api/gdrive/count?fid=1W9gf3ReGUboJUah-7XDg5jKXKl5XwQQ3)) 4 | 共 242 个文件和 26 个文件夹 5 | 6 | 如无特殊说明,以下运行环境都是在本地命令行(挂代理) 7 | 8 | ## 本工具耗时 40 秒 9 | 10 | ![](static/gdurl.png) 11 | 12 | 另外我在一台洛杉矶的vps上执行相同的命令,耗时23秒。 13 | 这个速度是在使用本项目默认配置**20个并行请求**得出来的,此值可自行修改(下文有方法),并行请求数越大,总速度越快。 14 | 15 | ## AutoRclone 耗时 4 分 57 秒(去掉拷贝后验证时间 4 分 6 秒) 16 | 17 | ![](static/autorclone.png) 18 | 19 | ## gclone 耗时 3 分 7 秒 20 | 21 | ![](static/gclone.png) 22 | 23 | ## 为什么速度会有这么大差异 24 | 首先要明确一下 server side copy(后称ssc) 的原理。 25 | 26 | 对于 Google Drive 本身而言,它不会因为你ssc复制了一份文件而真的去在自己的文件系统上复制一遍(否则不管它有多大硬盘都会被填满),它只是在数据库里添上了一笔记录。 27 | 28 | 所以,无论ssc一份大文件还是小文件,理论上它的耗时都是一样的。 29 | 各位在使用这些工具的时候也可以感受到,复制一堆小文件比复制几个大文件要慢得多。 30 | 31 | Google Drive 官方的 API 只提供了复制单个文件的功能,无法直接复制整个文件夹。甚至也无法读取整个文件夹,只能读取某个文件夹的第一层子文件(夹)信息,类似 Linux 命令行里的 `ls` 命令。 32 | 33 | 这三个工具的ssc功能,本质上都是对[官方file copy api](https://developers.google.com/drive/api/v3/reference/files/copy)的调用。 34 | 35 | 然后说一下本工具的原理,其大概步骤如下: 36 | 37 | - 首先,它会递归读取要复制的目录里的所有文件和文件夹的信息,并保存到本地。 38 | - 然后,将所有文件夹对象过滤出来,再根据彼此的父子关系,创建新的同名文件夹,还原出原始结构。(在保证速度的同时保持原始文件夹结构不变,这真的费了一番功夫) 39 | - 根据上一步创建文件夹时留下的新旧文件夹ID的对应关系,调用官方API复制文件。 40 | 41 | 得益于本地数据库的存在,它可以在任务中断后从断点继续执行。比如用户按下`ctrl+c`后,可以再执行一遍相同的拷贝命令,本工具会给出三个选项: 42 | 43 | ![](static/choose.png) 44 | 45 | 另外两个工具也支持断点续传,它们是怎样做到的呢?AutoRclone是用python对rclone命令的一层封装,gclone是基于rclone的魔改。 46 | 对了——值得一提的是——本工具是直接调用的官方API,不依赖于rclone。 47 | 48 | 我没有仔细阅读过rclone的源码,但是从它的执行日志中可以大概猜出其工作原理。 49 | 先补充个背景知识:对于存在于Google drive的所有文件(夹)对象,它们的一生都伴随着一个独一无二的ID,就算一个文件是另一个的拷贝,它们的ID也不一样。 50 | 51 | 所以rclone是怎么知道哪些文件拷贝过,哪些没有呢?如果它没有像我一样将记录保存在本地数据库的话,那么它只能在同一路径下搜索是否存在同名文件,如果存在,再比对它们的 大小/修改时间/md5值 等判断是否拷贝过。 52 | 53 | 也就是说,在最坏的情况下(假设它没做缓存),它每拷贝一个文件之前,都要先调用官方API来搜索判断此文件是否已存在! 54 | 55 | 此外,AutoRclone和gclone虽然都支持自动切换service account,但是它们执行拷贝任务的时候都是单一SA在调用API,这就注定了它们不能把请求频率调太高——否则可能触发限制。 56 | 57 | 而本工具同样支持自动切换service account,区别在于它的每次请求都是随机选一个SA,我的[文件统计](https://gdurl.viegg.com/api/gdrive/count?fid=1W9gf3ReGUboJUah-7XDg5jKXKl5XwQQ3)接口就用了20个SA的token,同时请求数设置成20个,也就是平均而言,单个SA的并发请求数只有一次。 58 | 59 | 所以瓶颈不在于SA的频率限制,而在运行的vps或代理上,各位可以根据各自的情况适当调整 PARALLEL_LIMIT 的值(在 `config.js` 里)。 60 | 61 | 当然,如果某个SA的单日流量超过了750G,会自动切换成别的SA,同时过滤掉流量用尽的SA。当所有SA流量用完后,会报错提示并退出。 62 | 63 | *使用SA存在的限制:除了每日流量限制外,其实每个SA还有个**15G的个人盘空间限额**,也就是说你每个SA最多能拷贝15G的文件到个人盘,但是拷贝到团队盘则无此限制。* 64 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | // 单次请求多少毫秒未响应以后超时(基准值,若连续超时则下次调整为上次的2倍) 2 | const TIMEOUT_BASE = 7000 3 | // 最大超时设置,比如某次请求,第一次7s超时,第二次14s,第三次28s,第四次56s,第五次不是112s而是60s,后续同理 4 | const TIMEOUT_MAX = 60000 5 | 6 | const LOG_DELAY = 5000 // 日志输出时间间隔,单位毫秒 7 | const PAGE_SIZE = 1000 // 每次网络请求读取目录下的文件数,数值越大,越有可能超时,不得超过1000 8 | 9 | const RETRY_LIMIT = 7 // 如果某次请求失败,允许其重试的最大次数 10 | const PARALLEL_LIMIT = 20 // 网络请求的并行数量,可根据网络环境调整 11 | 12 | const DEFAULT_TARGET = '' // 必填,拷贝默认目的地ID,如果不指定target,则会复制到此处,建议填写团队盘ID 13 | 14 | const AUTH = { // 如果您拥有service account的json授权文件,可将其拷贝至 sa 目录中以代替 client_id/secret/refrest_token 15 | client_id: 'your_client_id', 16 | client_secret: 'your_client_secret', 17 | refresh_token: 'your_refrest_token', 18 | expires: 0, // 可以留空 19 | access_token: '', // 可以留空 20 | tg_token: 'bot_token', // 你的 telegram robot 的 token,获取方法参见 https://core.telegram.org/bots#6-botfather 21 | tg_whitelist: ['your_tg_username'] // 你的tg username(t.me/username),bot只会执行这个列表里的用户所发送的指令 22 | } 23 | 24 | module.exports = { AUTH, PARALLEL_LIMIT, RETRY_LIMIT, TIMEOUT_BASE, TIMEOUT_MAX, LOG_DELAY, PAGE_SIZE, DEFAULT_TARGET } 25 | -------------------------------------------------------------------------------- /copy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const bytes = require('bytes') 4 | 5 | const { argv } = require('yargs') 6 | .usage('用法: ./$0 [options]\ntarget id可选,不填则使用config.js里的DEFAULT_TARGET') 7 | .alias('u', 'update') 8 | .describe('u', '不使用本地缓存,强制从线上获取源文件夹信息') 9 | .alias('y', 'yes') 10 | .describe('yes', '若发现拷贝记录,不询问直接继续上次的进度') 11 | .alias('f', 'file') 12 | .describe('f', '复制单个文件') 13 | .alias('n', 'name') 14 | .describe('n', '给目标文件夹重命名,不填则保留原始目录名') 15 | .alias('N', 'not_teamdrive') 16 | .describe('N', '如果不是团队盘链接,可以加上此参数以提高接口查询效率,降低延迟') 17 | .alias('s', 'size') 18 | .describe('s', '不填默认拷贝全部文件,如果设置了这个值,则过滤掉小于这个size的文件,必须以b结尾,比如10mb') 19 | .alias('S', 'service_account') 20 | .describe('S', '指定使用service account进行操作,前提是必须在 ./sa 目录下放置json授权文件,请确保sa帐号拥有操作权限。') 21 | .alias('D', 'dncnr') 22 | .describe('D', 'do not create new root, 不在目的地创建同名文件夹,直接将源文件夹中的文件原样复制到目的文件夹中') 23 | .help('h') 24 | .alias('h', 'help') 25 | 26 | const { copy, copy_file, validate_fid } = require('./src/gd') 27 | const { DEFAULT_TARGET } = require('./config') 28 | 29 | let [source, target] = argv._ 30 | 31 | if (validate_fid(source)) { 32 | const { name, update, file, not_teamdrive, size, service_account, dncnr } = argv 33 | if (file) { 34 | target = target || DEFAULT_TARGET 35 | if (!validate_fid(target)) throw new Error('target id 格式不正确') 36 | return copy_file(source, target, service_account).then(r => { 37 | const link = 'https://drive.google.com/drive/folders/' + target 38 | console.log('任务完成,文件所在位置:\n', link) 39 | }).catch(console.error) 40 | } 41 | let min_size 42 | if (size) { 43 | console.log(`不复制大小低于 ${size} 的文件`) 44 | min_size = bytes.parse(size) 45 | } 46 | copy({ source, target, name, min_size, update, not_teamdrive, service_account, dncnr }).then(folder => { 47 | if (!folder) return 48 | const link = 'https://drive.google.com/drive/folders/' + folder.id 49 | console.log('\n任务完成,新文件夹链接:\n', link) 50 | }) 51 | } else { 52 | console.warn('目录ID缺失或格式错误') 53 | } 54 | -------------------------------------------------------------------------------- /count: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { argv } = require('yargs') 4 | .usage('用法: ./$0 <目录ID> [options]') 5 | .example('./$0 1ULY8ISgWSOVc0UrzejykVgXfVL_I4r75', '获取 https://drive.google.com/drive/folders/1ULY8ISgWSOVc0UrzejykVgXfVL_I4r75 内包含的的所有文件的统计信息') 6 | .example('./$0 root -s size -t html -o out.html', '获取个人盘根目录统计信息,结果以HTML表格输出,根据总大小逆序排列,保存到本目录下的out.html文件中(不存在则新建,存在则覆盖)') 7 | .example('./$0 root -s name -t json -o out.json', '获取个人盘根目录统计信息,结果以JSON格式输出,根据文件扩展名排序,保存到本目录下的out.json文件中') 8 | .example('./$0 root -t all -o all.json', '获取个人盘根目录统计信息,将所有文件信息(包括文件夹)以JSON格式输出,保存到本目录下的all.json文件中') 9 | .alias('u', 'update') 10 | .describe('u', '强制从线上获取信息(无视是否存在本地缓存)') 11 | .alias('N', 'not_teamdrive') 12 | .describe('N', '如果不是团队盘链接,可以加上此参数以提高接口查询效率,降低延迟。如果要统计的是个人盘且./sa目录下的service account没有相关权限,请确保加上此参数以使用个人的auth信息进行查询') 13 | .alias('S', 'service_account') 14 | .describe('S', '指定使用service account进行统计,前提是必须在sa目录下放置SA json文件') 15 | .alias('s', 'sort') 16 | .describe('s', '统计结果排序方法,可选值 name 或 size,不填则默认根据文件数量逆序排列') 17 | .alias('t', 'type') 18 | .describe('t', '统计结果输出类型,可选值 html/tree/json/all,all表示输出所有文件json数据,最好搭配 -o 使用。不填则默认输出命令行表格') 19 | .alias('o', 'output') 20 | .describe('o', '统计结果输出文件,适合搭配 -t 使用') 21 | .help('h') 22 | .alias('h', 'help') 23 | 24 | const { count, validate_fid } = require('./src/gd') 25 | const [fid] = argv._ 26 | if (validate_fid(fid)) { 27 | const { update, sort, type, output, not_teamdrive, service_account } = argv 28 | count({ fid, update, sort, type, output, not_teamdrive, service_account }).catch(console.error) 29 | } else { 30 | console.warn('目录ID缺失或格式错误') 31 | } 32 | -------------------------------------------------------------------------------- /create-table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "gd" ( 2 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, 3 | "fid" TEXT NOT NULL UNIQUE, 4 | "info" TEXT, 5 | "summary" TEXT, 6 | "subf" TEXT, 7 | "ctime" INTEGER, 8 | "mtime" INTEGER 9 | ); 10 | 11 | CREATE UNIQUE INDEX "gd_fid" ON "gd" ( 12 | "fid" 13 | ); 14 | 15 | CREATE TABLE "task" ( 16 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, 17 | "source" TEXT NOT NULL, 18 | "target" TEXT NOT NULL, 19 | "status" TEXT, 20 | "copied" TEXT DEFAULT '', 21 | "mapping" TEXT DEFAULT '', 22 | "ctime" INTEGER, 23 | "ftime" INTEGER 24 | ); 25 | 26 | CREATE UNIQUE INDEX "task_source_target" ON "task" ( 27 | "source", 28 | "target" 29 | ); 30 | 31 | CREATE TABLE "copied" ( 32 | "taskid" INTEGER, 33 | "fileid" TEXT 34 | ); 35 | 36 | CREATE INDEX "copied_taskid" ON "copied" ("taskid"); 37 | 38 | CREATE TABLE "bookmark" ( 39 | "alias" TEXT, 40 | "target" TEXT 41 | ); 42 | 43 | CREATE UNIQUE INDEX "bookmark_alias" ON "bookmark" ( 44 | "alias" 45 | ); 46 | -------------------------------------------------------------------------------- /db.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const db_location = path.join(__dirname, 'gdurl.sqlite') 3 | const db = require('better-sqlite3')(db_location) 4 | 5 | db.pragma('journal_mode = WAL') 6 | 7 | create_table_copied() 8 | function create_table_copied () { 9 | const [exists] = db.prepare('PRAGMA table_info(copied)').all() 10 | if (exists) return 11 | const create_table = `CREATE TABLE "copied" ( 12 | "taskid" INTEGER, 13 | "fileid" TEXT 14 | )` 15 | db.prepare(create_table).run() 16 | const create_index = `CREATE INDEX "copied_taskid" ON "copied" ("taskid");` 17 | db.prepare(create_index).run() 18 | } 19 | 20 | create_table_bookmark() 21 | function create_table_bookmark () { 22 | const [exists] = db.prepare('PRAGMA table_info(bookmark)').all() 23 | if (exists) return 24 | const create_table = `CREATE TABLE "bookmark" ( 25 | "alias" TEXT, 26 | "target" TEXT 27 | );` 28 | db.prepare(create_table).run() 29 | const create_index = `CREATE UNIQUE INDEX "bookmark_alias" ON "bookmark" ( 30 | "alias" 31 | );` 32 | db.prepare(create_index).run() 33 | } 34 | 35 | module.exports = { db } 36 | -------------------------------------------------------------------------------- /dedupe: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { argv } = require('yargs') 4 | .usage('用法: ./$0 [options]') 5 | .alias('y', 'yes') 6 | .describe('yes', '若发现重复项,不询问直接删除') 7 | .alias('u', 'update') 8 | .describe('u', '不使用本地缓存,强制从线上获取源文件夹信息') 9 | .alias('S', 'service_account') 10 | .describe('S', '使用service account进行操作,前提是必须在 ./sa 目录下放置sa授权json文件') 11 | .help('h') 12 | .alias('h', 'help') 13 | 14 | const { dedupe, validate_fid } = require('./src/gd') 15 | 16 | const [fid] = argv._ 17 | if (validate_fid(fid)) { 18 | const { update, service_account, yes } = argv 19 | dedupe({ fid, update, service_account, yes }).then(info => { 20 | if (!info) return 21 | const { file_count, folder_count } = info 22 | console.log('任务完成,共删除文件数:', file_count, '目录数:', folder_count) 23 | }) 24 | } else { 25 | console.warn('目录ID缺失或格式错误') 26 | } 27 | -------------------------------------------------------------------------------- /doc/bot-worked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErics/gd-utils/f4c8450a3f81ba65c7208031d9fdfbb2eeb7980c/doc/bot-worked.png -------------------------------------------------------------------------------- /doc/tgbot-appache2-note.md: -------------------------------------------------------------------------------- 1 | # 几个坑 2 | * Telegram Bot API 提供了两种方式, webhook 和 long polling,目前项目只支持 webhook 方式。 3 | * webhook 方式必须要用HTTPS 也就是需要准备**个人域名**和**一个有效证书** 4 | * 证书一定要单独域名证书(泛域名证书不能用) 5 | 6 | 7 | 8 | # 原理/思路 9 | TG创建bot,要起一个服务支持BOT的功能, 所以需要配置webhook 让tg 和服务器建立连接。webhook 需要有HTTPS的外网域名并且修改DNS指向你所配置的服务器IP,这样就能保证TG的请求可以顺利到达并且验证BOT。 10 | 在服务器内部如果如果是单BOT, 可以直接用nodje 配合 PM2 直接起服务,然后修改server.js端口号443。 如果服务器上有多个服务,那么就需要用反向代理,反代简单说就是一个服务+映射规则 (ngnix或者apache后者其他都可以) 侦听80或者443端口,如果有指定的映射请求, 就转发到内部映射的各个服务。 11 | 12 | 例如 13 | ``` 14 | aaa.domain.com <=> locahost:3001 15 | bbb.domain.com <=> locahost:3002 16 | domain.com/ccc <=> localhost:3003 17 | ``` 18 | 19 | 20 | 21 | # 步骤 22 | 1. 需要去tg 创建一个bot,会得到token 和bot的tgurl 23 | 2. BOT服务: 24 | 1. 服务器上clone 项目,安装node, npm install 25 | 2. 如果需要配置多个BOT, clone不同目录, server.js里修改配置port,和config.js 26 | 3. 安装PM2,在每个bot目录下 PM2 start server.js 27 | 4. ``` pm2 status``` 确认服务跑起来了 28 | 1. 如果没起来, 查log文件(见底部) 29 | 5. curl 检查本地连接, curl 检查远端连接, not found 就对了 30 | 3. 外部连接 31 | 1. 修改DNS,我是用cloudflare 把添加A record, 直接把静态IP 绑定 32 | 2. 绑定以后, 本地开个terminal, ping 刚添加域名,直到解析的IP是你绑定的,这步确保连接上是畅通的 33 | 4. apache2开启SSL和反代 34 | 1. 复制证书到任意位置 35 | 2. 运行底部命令 36 | 3. /etc/apache2/sites-available 下找到默认的.conf,或者自己建个conf也行 37 | 4. 修改底部配置信息 38 | 5. 保存重启 ```service apache2 restart``` 39 | 5. 剩下的就是配置和检查webhook,这里面也有不少坑,在反代配置文件部分。。记不清了。。 40 | 6. 如果一切顺利 /help 会弹出目录 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ``` 49 | pm2 部分 50 | 51 | tail -200 ~/.pm2/logs/server-error.log 52 | tail -200 ~/.pm2/logs/server-out.log 53 | 54 | curl "localhost:23333" 55 | curl "domain:23333" 56 | 57 | SSL+反代 58 | 59 | sudo a2enmod ssl 60 | sudo a2enmod proxy 61 | sudo a2enmod proxy_balancer 62 | sudo a2enmod proxy_http 63 | 64 | 65 | /etc/apache2/sites-available/xxx.conf 66 | 67 | 68 | SSLEngine on 69 | SSLProtocol all 70 | SSLCertificateFile {{CERT_DIR}}/{{domain.cer}} 71 | SSLCertificateKeyFile {{CERT_DIR}}/{{domain.key}} 72 | SSLCACertificateFile {{CERT_DIR}}/{{domain.ca.cer}} 73 | 74 | ServerName {{domain}} 75 | 76 | ProxyRequests Off 77 | ProxyPreserveHost On 78 | ProxyVia Full 79 | 80 | 81 | Require all granted 82 | 83 | # 这里我用的是子目录映射方式。懒得再申请一个证书。。domain.com/ccc <=> localhost:3003 84 | ProxyPass /{{bot1url}}/ http://127.0.0.1:23334/ # bot1 85 | ProxyPassReverse /{{bot1url}}/ http://127.0.0.1:23334/ # bot1 86 | ProxyPass /{{bot2url}}/ http://127.0.0.1:23333/ # bot2 87 | ProxyPassReverse /{{bot2url}}/ http://127.0.0.1:23333/ # bot2 88 | 89 | 90 | 91 | something for verify and DEBUG 92 | 93 | Apache command: 94 | service apache2 restart 95 | service apache2 stop 96 | service apache2 status 97 | service apache2 reload 98 | tail -100 /var/log/apache2/error.log 99 | 100 | 101 | 验证一下SSL: 102 | https://www.ssllabs.com/ssltest/analyze.html 确保Trusted和In trust store是绿的(反正我这两个绿的就TG就能找到的到) 103 | 104 | SET webhook 105 | 106 | curl -F "url=https://{{domain}}/{{bot1url}}/api/gdurl/tgbot" 'https://api.telegram.org/bot{{BOT_TOKEN}}/setWebhook' 107 | 108 | delete webhook 109 | curl -F "url=" https://api.telegram.org/bot{{BOT_TOKEN}}/setWebhook 110 | 111 | 112 | check webhook 113 | curl "https://api.telegram.org/bot{{BOT_TOKEN}}/getWebhookInfo" 114 | 115 | 116 | 117 | ``` 118 | 119 | 120 | ![avatar](/doc/bot-worked.png) 121 | 122 | 123 | # Reference Link 124 | 125 | https://core.telegram.org/bots 126 | 127 | https://core.telegram.org/bots/api 128 | 129 | https://www.jianshu.com/p/ca804497afa0 130 | -------------------------------------------------------------------------------- /gdurl.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErics/gd-utils/f4c8450a3f81ba65c7208031d9fdfbb2eeb7980c/gdurl.sqlite -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gd-utils", 3 | "version": "1.2.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@koa/router": { 8 | "version": "9.0.1", 9 | "resolved": "https://registry.npmjs.org/@koa/router/-/router-9.0.1.tgz", 10 | "integrity": "sha512-OI+OU49CJV4px0WkIMmayBeqVXB/JS1ZMq7UoGlTZt6Y7ijK7kdeQ18+SEHHJPytmtI1y6Hf8XLrpxva3mhv5Q==", 11 | "requires": { 12 | "debug": "^4.1.1", 13 | "http-errors": "^1.7.3", 14 | "koa-compose": "^4.1.0", 15 | "methods": "^1.1.2", 16 | "path-to-regexp": "^6.1.0" 17 | } 18 | }, 19 | "@types/color-name": { 20 | "version": "1.1.1", 21 | "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", 22 | "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" 23 | }, 24 | "@viegg/axios": { 25 | "version": "1.0.0", 26 | "resolved": "https://registry.npmjs.org/@viegg/axios/-/axios-1.0.0.tgz", 27 | "integrity": "sha512-BCLyhXPaZ/8E5z8VeKSnY5h21AHd3yAaqd0Zw/eNrPwEnJB+Geju8f8L3S8Ww9iHpB1w2wG2cXz/zSdxPRPqBA==" 28 | }, 29 | "abort-controller": { 30 | "version": "3.0.0", 31 | "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", 32 | "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", 33 | "requires": { 34 | "event-target-shim": "^5.0.0" 35 | } 36 | }, 37 | "accepts": { 38 | "version": "1.3.7", 39 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 40 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 41 | "requires": { 42 | "mime-types": "~2.1.24", 43 | "negotiator": "0.6.2" 44 | } 45 | }, 46 | "agent-base": { 47 | "version": "6.0.0", 48 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz", 49 | "integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==", 50 | "requires": { 51 | "debug": "4" 52 | } 53 | }, 54 | "ansi-regex": { 55 | "version": "2.1.1", 56 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 57 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 58 | }, 59 | "ansi-styles": { 60 | "version": "4.2.1", 61 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", 62 | "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", 63 | "requires": { 64 | "@types/color-name": "^1.1.1", 65 | "color-convert": "^2.0.1" 66 | } 67 | }, 68 | "any-promise": { 69 | "version": "1.3.0", 70 | "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", 71 | "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" 72 | }, 73 | "aproba": { 74 | "version": "1.2.0", 75 | "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", 76 | "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" 77 | }, 78 | "are-we-there-yet": { 79 | "version": "1.1.5", 80 | "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", 81 | "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", 82 | "requires": { 83 | "delegates": "^1.0.0", 84 | "readable-stream": "^2.0.6" 85 | } 86 | }, 87 | "base64-js": { 88 | "version": "1.3.1", 89 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", 90 | "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" 91 | }, 92 | "better-sqlite3": { 93 | "version": "7.1.0", 94 | "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-7.1.0.tgz", 95 | "integrity": "sha512-FV/snQ8F/kyqhdxsevzbojVtMowDWOfe1A5N3lYu1KJwoho2t7JgITmdlSc7DkOh3Zq65I+ZyeNWXQrkLEDFTg==", 96 | "requires": { 97 | "bindings": "^1.5.0", 98 | "prebuild-install": "^5.3.3", 99 | "tar": "4.4.10" 100 | } 101 | }, 102 | "bindings": { 103 | "version": "1.5.0", 104 | "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", 105 | "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", 106 | "requires": { 107 | "file-uri-to-path": "1.0.0" 108 | } 109 | }, 110 | "bl": { 111 | "version": "4.0.2", 112 | "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", 113 | "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", 114 | "requires": { 115 | "buffer": "^5.5.0", 116 | "inherits": "^2.0.4", 117 | "readable-stream": "^3.4.0" 118 | }, 119 | "dependencies": { 120 | "readable-stream": { 121 | "version": "3.6.0", 122 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 123 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 124 | "requires": { 125 | "inherits": "^2.0.3", 126 | "string_decoder": "^1.1.1", 127 | "util-deprecate": "^1.0.1" 128 | } 129 | } 130 | } 131 | }, 132 | "buffer": { 133 | "version": "5.6.0", 134 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", 135 | "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", 136 | "requires": { 137 | "base64-js": "^1.0.2", 138 | "ieee754": "^1.1.4" 139 | } 140 | }, 141 | "buffer-equal-constant-time": { 142 | "version": "1.0.1", 143 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 144 | "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" 145 | }, 146 | "bytes": { 147 | "version": "3.1.0", 148 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 149 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 150 | }, 151 | "cache-content-type": { 152 | "version": "1.0.1", 153 | "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", 154 | "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", 155 | "requires": { 156 | "mime-types": "^2.1.18", 157 | "ylru": "^1.2.0" 158 | } 159 | }, 160 | "camelcase": { 161 | "version": "5.3.1", 162 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", 163 | "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" 164 | }, 165 | "chownr": { 166 | "version": "1.1.4", 167 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 168 | "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" 169 | }, 170 | "cli-table3": { 171 | "version": "0.6.0", 172 | "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.0.tgz", 173 | "integrity": "sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==", 174 | "requires": { 175 | "colors": "^1.1.2", 176 | "object-assign": "^4.1.0", 177 | "string-width": "^4.2.0" 178 | }, 179 | "dependencies": { 180 | "ansi-regex": { 181 | "version": "5.0.0", 182 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", 183 | "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" 184 | }, 185 | "is-fullwidth-code-point": { 186 | "version": "3.0.0", 187 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 188 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 189 | }, 190 | "string-width": { 191 | "version": "4.2.0", 192 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", 193 | "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", 194 | "requires": { 195 | "emoji-regex": "^8.0.0", 196 | "is-fullwidth-code-point": "^3.0.0", 197 | "strip-ansi": "^6.0.0" 198 | } 199 | }, 200 | "strip-ansi": { 201 | "version": "6.0.0", 202 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", 203 | "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", 204 | "requires": { 205 | "ansi-regex": "^5.0.0" 206 | } 207 | } 208 | } 209 | }, 210 | "cliui": { 211 | "version": "6.0.0", 212 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", 213 | "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", 214 | "requires": { 215 | "string-width": "^4.2.0", 216 | "strip-ansi": "^6.0.0", 217 | "wrap-ansi": "^6.2.0" 218 | }, 219 | "dependencies": { 220 | "ansi-regex": { 221 | "version": "5.0.0", 222 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", 223 | "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" 224 | }, 225 | "is-fullwidth-code-point": { 226 | "version": "3.0.0", 227 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 228 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 229 | }, 230 | "string-width": { 231 | "version": "4.2.0", 232 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", 233 | "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", 234 | "requires": { 235 | "emoji-regex": "^8.0.0", 236 | "is-fullwidth-code-point": "^3.0.0", 237 | "strip-ansi": "^6.0.0" 238 | } 239 | }, 240 | "strip-ansi": { 241 | "version": "6.0.0", 242 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", 243 | "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", 244 | "requires": { 245 | "ansi-regex": "^5.0.0" 246 | } 247 | } 248 | } 249 | }, 250 | "co": { 251 | "version": "4.6.0", 252 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 253 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" 254 | }, 255 | "co-body": { 256 | "version": "6.0.0", 257 | "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.0.0.tgz", 258 | "integrity": "sha512-9ZIcixguuuKIptnY8yemEOuhb71L/lLf+Rl5JfJEUiDNJk0e02MBt7BPxR2GEh5mw8dPthQYR4jPI/BnS1MQgw==", 259 | "requires": { 260 | "inflation": "^2.0.0", 261 | "qs": "^6.5.2", 262 | "raw-body": "^2.3.3", 263 | "type-is": "^1.6.16" 264 | } 265 | }, 266 | "code-point-at": { 267 | "version": "1.1.0", 268 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", 269 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" 270 | }, 271 | "color-convert": { 272 | "version": "2.0.1", 273 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 274 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 275 | "requires": { 276 | "color-name": "~1.1.4" 277 | } 278 | }, 279 | "color-name": { 280 | "version": "1.1.4", 281 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 282 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 283 | }, 284 | "colors": { 285 | "version": "1.4.0", 286 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", 287 | "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" 288 | }, 289 | "console-control-strings": { 290 | "version": "1.1.0", 291 | "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", 292 | "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" 293 | }, 294 | "content-disposition": { 295 | "version": "0.5.3", 296 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 297 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 298 | "requires": { 299 | "safe-buffer": "5.1.2" 300 | } 301 | }, 302 | "content-type": { 303 | "version": "1.0.4", 304 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 305 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 306 | }, 307 | "cookies": { 308 | "version": "0.8.0", 309 | "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", 310 | "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", 311 | "requires": { 312 | "depd": "~2.0.0", 313 | "keygrip": "~1.1.0" 314 | }, 315 | "dependencies": { 316 | "depd": { 317 | "version": "2.0.0", 318 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 319 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" 320 | } 321 | } 322 | }, 323 | "copy-to": { 324 | "version": "2.0.1", 325 | "resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", 326 | "integrity": "sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=" 327 | }, 328 | "core-util-is": { 329 | "version": "1.0.2", 330 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 331 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 332 | }, 333 | "dayjs": { 334 | "version": "1.8.28", 335 | "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.28.tgz", 336 | "integrity": "sha512-ccnYgKC0/hPSGXxj7Ju6AV/BP4HUkXC2u15mikXT5mX9YorEaoi1bEKOmAqdkJHN4EEkmAf97SpH66Try5Mbeg==" 337 | }, 338 | "debug": { 339 | "version": "4.1.1", 340 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 341 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 342 | "requires": { 343 | "ms": "^2.1.1" 344 | } 345 | }, 346 | "decamelize": { 347 | "version": "1.2.0", 348 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", 349 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" 350 | }, 351 | "decompress-response": { 352 | "version": "4.2.1", 353 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", 354 | "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", 355 | "requires": { 356 | "mimic-response": "^2.0.0" 357 | } 358 | }, 359 | "deep-equal": { 360 | "version": "1.0.1", 361 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", 362 | "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" 363 | }, 364 | "deep-extend": { 365 | "version": "0.6.0", 366 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 367 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" 368 | }, 369 | "delegates": { 370 | "version": "1.0.0", 371 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 372 | "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" 373 | }, 374 | "depd": { 375 | "version": "1.1.2", 376 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 377 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 378 | }, 379 | "destroy": { 380 | "version": "1.0.4", 381 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 382 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 383 | }, 384 | "detect-libc": { 385 | "version": "1.0.3", 386 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", 387 | "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" 388 | }, 389 | "ecdsa-sig-formatter": { 390 | "version": "1.0.11", 391 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 392 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 393 | "requires": { 394 | "safe-buffer": "^5.0.1" 395 | } 396 | }, 397 | "ee-first": { 398 | "version": "1.1.1", 399 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 400 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 401 | }, 402 | "emoji-regex": { 403 | "version": "8.0.0", 404 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 405 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 406 | }, 407 | "encodeurl": { 408 | "version": "1.0.2", 409 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 410 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 411 | }, 412 | "end-of-stream": { 413 | "version": "1.4.4", 414 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 415 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 416 | "requires": { 417 | "once": "^1.4.0" 418 | } 419 | }, 420 | "escape-html": { 421 | "version": "1.0.3", 422 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 423 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 424 | }, 425 | "event-target-shim": { 426 | "version": "5.0.1", 427 | "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", 428 | "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" 429 | }, 430 | "expand-template": { 431 | "version": "2.0.3", 432 | "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", 433 | "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" 434 | }, 435 | "extend": { 436 | "version": "3.0.2", 437 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 438 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 439 | }, 440 | "file-uri-to-path": { 441 | "version": "1.0.0", 442 | "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", 443 | "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" 444 | }, 445 | "find-up": { 446 | "version": "4.1.0", 447 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", 448 | "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", 449 | "requires": { 450 | "locate-path": "^5.0.0", 451 | "path-exists": "^4.0.0" 452 | } 453 | }, 454 | "fresh": { 455 | "version": "0.5.2", 456 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 457 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 458 | }, 459 | "fs-constants": { 460 | "version": "1.0.0", 461 | "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 462 | "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" 463 | }, 464 | "fs-minipass": { 465 | "version": "1.2.7", 466 | "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", 467 | "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", 468 | "requires": { 469 | "minipass": "^2.6.0" 470 | } 471 | }, 472 | "gauge": { 473 | "version": "2.7.4", 474 | "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", 475 | "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", 476 | "requires": { 477 | "aproba": "^1.0.3", 478 | "console-control-strings": "^1.0.0", 479 | "has-unicode": "^2.0.0", 480 | "object-assign": "^4.1.0", 481 | "signal-exit": "^3.0.0", 482 | "string-width": "^1.0.1", 483 | "strip-ansi": "^3.0.1", 484 | "wide-align": "^1.1.0" 485 | } 486 | }, 487 | "gaxios": { 488 | "version": "3.0.3", 489 | "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.0.3.tgz", 490 | "integrity": "sha512-PkzQludeIFhd535/yucALT/Wxyj/y2zLyrMwPcJmnLHDugmV49NvAi/vb+VUq/eWztATZCNcb8ue+ywPG+oLuw==", 491 | "requires": { 492 | "abort-controller": "^3.0.0", 493 | "extend": "^3.0.2", 494 | "https-proxy-agent": "^5.0.0", 495 | "is-stream": "^2.0.0", 496 | "node-fetch": "^2.3.0" 497 | } 498 | }, 499 | "get-caller-file": { 500 | "version": "2.0.5", 501 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 502 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" 503 | }, 504 | "github-from-package": { 505 | "version": "0.0.0", 506 | "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", 507 | "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" 508 | }, 509 | "google-p12-pem": { 510 | "version": "3.0.1", 511 | "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.1.tgz", 512 | "integrity": "sha512-VlQgtozgNVVVcYTXS36eQz4PXPt9gIPqLOhHN0QiV6W6h4qSCNVKPtKC5INtJsaHHF2r7+nOIa26MJeJMTaZEQ==", 513 | "requires": { 514 | "node-forge": "^0.9.0" 515 | } 516 | }, 517 | "gtoken": { 518 | "version": "5.0.1", 519 | "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.0.1.tgz", 520 | "integrity": "sha512-33w4FNDkUcyIOq/TqyC+drnKdI4PdXmWp9lZzssyEQKuvu9ZFN3KttaSnDKo52U3E51oujVGop93mKxmqO8HHg==", 521 | "requires": { 522 | "gaxios": "^3.0.0", 523 | "google-p12-pem": "^3.0.0", 524 | "jws": "^4.0.0", 525 | "mime": "^2.2.0" 526 | } 527 | }, 528 | "has-unicode": { 529 | "version": "2.0.1", 530 | "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", 531 | "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" 532 | }, 533 | "html-escaper": { 534 | "version": "3.0.0", 535 | "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.0.tgz", 536 | "integrity": "sha512-69CofXDozHqdHDl1BZ3YiFp5rYN1qTwSXIVcBhVcZNkzj1vzx6Sko1nT58mzKip19DbKo8lHR9hf6/XeZ9+s3w==" 537 | }, 538 | "http-assert": { 539 | "version": "1.4.1", 540 | "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.1.tgz", 541 | "integrity": "sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw==", 542 | "requires": { 543 | "deep-equal": "~1.0.1", 544 | "http-errors": "~1.7.2" 545 | } 546 | }, 547 | "http-errors": { 548 | "version": "1.7.3", 549 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", 550 | "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", 551 | "requires": { 552 | "depd": "~1.1.2", 553 | "inherits": "2.0.4", 554 | "setprototypeof": "1.1.1", 555 | "statuses": ">= 1.5.0 < 2", 556 | "toidentifier": "1.0.0" 557 | } 558 | }, 559 | "https-proxy-agent": { 560 | "version": "5.0.0", 561 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", 562 | "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", 563 | "requires": { 564 | "agent-base": "6", 565 | "debug": "4" 566 | } 567 | }, 568 | "iconv-lite": { 569 | "version": "0.4.24", 570 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 571 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 572 | "requires": { 573 | "safer-buffer": ">= 2.1.2 < 3" 574 | } 575 | }, 576 | "ieee754": { 577 | "version": "1.1.13", 578 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", 579 | "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" 580 | }, 581 | "inflation": { 582 | "version": "2.0.0", 583 | "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", 584 | "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=" 585 | }, 586 | "inherits": { 587 | "version": "2.0.4", 588 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 589 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 590 | }, 591 | "ini": { 592 | "version": "1.3.5", 593 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", 594 | "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" 595 | }, 596 | "is-fullwidth-code-point": { 597 | "version": "1.0.0", 598 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 599 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 600 | "requires": { 601 | "number-is-nan": "^1.0.0" 602 | } 603 | }, 604 | "is-generator-function": { 605 | "version": "1.0.7", 606 | "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", 607 | "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==" 608 | }, 609 | "is-stream": { 610 | "version": "2.0.0", 611 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", 612 | "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" 613 | }, 614 | "isarray": { 615 | "version": "1.0.0", 616 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 617 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 618 | }, 619 | "jwa": { 620 | "version": "2.0.0", 621 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", 622 | "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", 623 | "requires": { 624 | "buffer-equal-constant-time": "1.0.1", 625 | "ecdsa-sig-formatter": "1.0.11", 626 | "safe-buffer": "^5.0.1" 627 | } 628 | }, 629 | "jws": { 630 | "version": "4.0.0", 631 | "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", 632 | "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", 633 | "requires": { 634 | "jwa": "^2.0.0", 635 | "safe-buffer": "^5.0.1" 636 | } 637 | }, 638 | "keygrip": { 639 | "version": "1.1.0", 640 | "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", 641 | "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", 642 | "requires": { 643 | "tsscmp": "1.0.6" 644 | } 645 | }, 646 | "kleur": { 647 | "version": "3.0.3", 648 | "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", 649 | "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" 650 | }, 651 | "koa": { 652 | "version": "2.13.0", 653 | "resolved": "https://registry.npmjs.org/koa/-/koa-2.13.0.tgz", 654 | "integrity": "sha512-i/XJVOfPw7npbMv67+bOeXr3gPqOAw6uh5wFyNs3QvJ47tUx3M3V9rIE0//WytY42MKz4l/MXKyGkQ2LQTfLUQ==", 655 | "requires": { 656 | "accepts": "^1.3.5", 657 | "cache-content-type": "^1.0.0", 658 | "content-disposition": "~0.5.2", 659 | "content-type": "^1.0.4", 660 | "cookies": "~0.8.0", 661 | "debug": "~3.1.0", 662 | "delegates": "^1.0.0", 663 | "depd": "^1.1.2", 664 | "destroy": "^1.0.4", 665 | "encodeurl": "^1.0.2", 666 | "escape-html": "^1.0.3", 667 | "fresh": "~0.5.2", 668 | "http-assert": "^1.3.0", 669 | "http-errors": "^1.6.3", 670 | "is-generator-function": "^1.0.7", 671 | "koa-compose": "^4.1.0", 672 | "koa-convert": "^1.2.0", 673 | "on-finished": "^2.3.0", 674 | "only": "~0.0.2", 675 | "parseurl": "^1.3.2", 676 | "statuses": "^1.5.0", 677 | "type-is": "^1.6.16", 678 | "vary": "^1.1.2" 679 | }, 680 | "dependencies": { 681 | "debug": { 682 | "version": "3.1.0", 683 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 684 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 685 | "requires": { 686 | "ms": "2.0.0" 687 | } 688 | }, 689 | "ms": { 690 | "version": "2.0.0", 691 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 692 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 693 | } 694 | } 695 | }, 696 | "koa-bodyparser": { 697 | "version": "4.3.0", 698 | "resolved": "https://registry.npmjs.org/koa-bodyparser/-/koa-bodyparser-4.3.0.tgz", 699 | "integrity": "sha512-uyV8G29KAGwZc4q/0WUAjH+Tsmuv9ImfBUF2oZVyZtaeo0husInagyn/JH85xMSxM0hEk/mbCII5ubLDuqW/Rw==", 700 | "requires": { 701 | "co-body": "^6.0.0", 702 | "copy-to": "^2.0.1" 703 | } 704 | }, 705 | "koa-compose": { 706 | "version": "4.1.0", 707 | "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", 708 | "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==" 709 | }, 710 | "koa-convert": { 711 | "version": "1.2.0", 712 | "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", 713 | "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", 714 | "requires": { 715 | "co": "^4.6.0", 716 | "koa-compose": "^3.0.0" 717 | }, 718 | "dependencies": { 719 | "koa-compose": { 720 | "version": "3.2.1", 721 | "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", 722 | "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", 723 | "requires": { 724 | "any-promise": "^1.1.0" 725 | } 726 | } 727 | } 728 | }, 729 | "locate-path": { 730 | "version": "5.0.0", 731 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", 732 | "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", 733 | "requires": { 734 | "p-locate": "^4.1.0" 735 | } 736 | }, 737 | "media-typer": { 738 | "version": "0.3.0", 739 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 740 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 741 | }, 742 | "methods": { 743 | "version": "1.1.2", 744 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 745 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 746 | }, 747 | "mime": { 748 | "version": "2.4.6", 749 | "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", 750 | "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" 751 | }, 752 | "mime-db": { 753 | "version": "1.44.0", 754 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", 755 | "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" 756 | }, 757 | "mime-types": { 758 | "version": "2.1.27", 759 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", 760 | "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", 761 | "requires": { 762 | "mime-db": "1.44.0" 763 | } 764 | }, 765 | "mimic-response": { 766 | "version": "2.1.0", 767 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", 768 | "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" 769 | }, 770 | "minimist": { 771 | "version": "1.2.5", 772 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 773 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 774 | }, 775 | "minipass": { 776 | "version": "2.9.0", 777 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", 778 | "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", 779 | "requires": { 780 | "safe-buffer": "^5.1.2", 781 | "yallist": "^3.0.0" 782 | } 783 | }, 784 | "minizlib": { 785 | "version": "1.3.3", 786 | "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", 787 | "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", 788 | "requires": { 789 | "minipass": "^2.9.0" 790 | } 791 | }, 792 | "mkdirp": { 793 | "version": "0.5.5", 794 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 795 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 796 | "requires": { 797 | "minimist": "^1.2.5" 798 | } 799 | }, 800 | "mkdirp-classic": { 801 | "version": "0.5.3", 802 | "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 803 | "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" 804 | }, 805 | "ms": { 806 | "version": "2.1.2", 807 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 808 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 809 | }, 810 | "napi-build-utils": { 811 | "version": "1.0.2", 812 | "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", 813 | "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" 814 | }, 815 | "negotiator": { 816 | "version": "0.6.2", 817 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 818 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 819 | }, 820 | "node-abi": { 821 | "version": "2.18.0", 822 | "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.18.0.tgz", 823 | "integrity": "sha512-yi05ZoiuNNEbyT/xXfSySZE+yVnQW6fxPZuFbLyS1s6b5Kw3HzV2PHOM4XR+nsjzkHxByK+2Wg+yCQbe35l8dw==", 824 | "requires": { 825 | "semver": "^5.4.1" 826 | } 827 | }, 828 | "node-fetch": { 829 | "version": "2.6.0", 830 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", 831 | "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" 832 | }, 833 | "node-forge": { 834 | "version": "0.9.1", 835 | "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.1.tgz", 836 | "integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==" 837 | }, 838 | "noop-logger": { 839 | "version": "0.1.1", 840 | "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", 841 | "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" 842 | }, 843 | "npmlog": { 844 | "version": "4.1.2", 845 | "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", 846 | "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", 847 | "requires": { 848 | "are-we-there-yet": "~1.1.2", 849 | "console-control-strings": "~1.1.0", 850 | "gauge": "~2.7.3", 851 | "set-blocking": "~2.0.0" 852 | } 853 | }, 854 | "number-is-nan": { 855 | "version": "1.0.1", 856 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 857 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" 858 | }, 859 | "object-assign": { 860 | "version": "4.1.1", 861 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 862 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 863 | }, 864 | "on-finished": { 865 | "version": "2.3.0", 866 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 867 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 868 | "requires": { 869 | "ee-first": "1.1.1" 870 | } 871 | }, 872 | "once": { 873 | "version": "1.4.0", 874 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 875 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 876 | "requires": { 877 | "wrappy": "1" 878 | } 879 | }, 880 | "only": { 881 | "version": "0.0.2", 882 | "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", 883 | "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" 884 | }, 885 | "p-limit": { 886 | "version": "3.0.1", 887 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.1.tgz", 888 | "integrity": "sha512-mw/p92EyOzl2MhauKodw54Rx5ZK4624rNfgNaBguFZkHzyUG9WsDzFF5/yQVEJinbJDdP4jEfMN+uBquiGnaLg==", 889 | "requires": { 890 | "p-try": "^2.0.0" 891 | } 892 | }, 893 | "p-locate": { 894 | "version": "4.1.0", 895 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", 896 | "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", 897 | "requires": { 898 | "p-limit": "^2.2.0" 899 | }, 900 | "dependencies": { 901 | "p-limit": { 902 | "version": "2.3.0", 903 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", 904 | "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", 905 | "requires": { 906 | "p-try": "^2.0.0" 907 | } 908 | } 909 | } 910 | }, 911 | "p-try": { 912 | "version": "2.2.0", 913 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", 914 | "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" 915 | }, 916 | "parseurl": { 917 | "version": "1.3.3", 918 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 919 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 920 | }, 921 | "path-exists": { 922 | "version": "4.0.0", 923 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 924 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" 925 | }, 926 | "path-to-regexp": { 927 | "version": "6.1.0", 928 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.1.0.tgz", 929 | "integrity": "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==" 930 | }, 931 | "prebuild-install": { 932 | "version": "5.3.5", 933 | "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.5.tgz", 934 | "integrity": "sha512-YmMO7dph9CYKi5IR/BzjOJlRzpxGGVo1EsLSUZ0mt/Mq0HWZIHOKHHcHdT69yG54C9m6i45GpItwRHpk0Py7Uw==", 935 | "requires": { 936 | "detect-libc": "^1.0.3", 937 | "expand-template": "^2.0.3", 938 | "github-from-package": "0.0.0", 939 | "minimist": "^1.2.3", 940 | "mkdirp": "^0.5.1", 941 | "napi-build-utils": "^1.0.1", 942 | "node-abi": "^2.7.0", 943 | "noop-logger": "^0.1.1", 944 | "npmlog": "^4.0.1", 945 | "pump": "^3.0.0", 946 | "rc": "^1.2.7", 947 | "simple-get": "^3.0.3", 948 | "tar-fs": "^2.0.0", 949 | "tunnel-agent": "^0.6.0", 950 | "which-pm-runs": "^1.0.0" 951 | } 952 | }, 953 | "process-nextick-args": { 954 | "version": "2.0.1", 955 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 956 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 957 | }, 958 | "prompts": { 959 | "version": "2.3.2", 960 | "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.3.2.tgz", 961 | "integrity": "sha512-Q06uKs2CkNYVID0VqwfAl9mipo99zkBv/n2JtWY89Yxa3ZabWSrs0e2KTudKVa3peLUvYXMefDqIleLPVUBZMA==", 962 | "requires": { 963 | "kleur": "^3.0.3", 964 | "sisteransi": "^1.0.4" 965 | } 966 | }, 967 | "pump": { 968 | "version": "3.0.0", 969 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 970 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 971 | "requires": { 972 | "end-of-stream": "^1.1.0", 973 | "once": "^1.3.1" 974 | } 975 | }, 976 | "qs": { 977 | "version": "6.9.4", 978 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", 979 | "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" 980 | }, 981 | "raw-body": { 982 | "version": "2.4.1", 983 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz", 984 | "integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==", 985 | "requires": { 986 | "bytes": "3.1.0", 987 | "http-errors": "1.7.3", 988 | "iconv-lite": "0.4.24", 989 | "unpipe": "1.0.0" 990 | } 991 | }, 992 | "rc": { 993 | "version": "1.2.8", 994 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 995 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 996 | "requires": { 997 | "deep-extend": "^0.6.0", 998 | "ini": "~1.3.0", 999 | "minimist": "^1.2.0", 1000 | "strip-json-comments": "~2.0.1" 1001 | } 1002 | }, 1003 | "readable-stream": { 1004 | "version": "2.3.7", 1005 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 1006 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 1007 | "requires": { 1008 | "core-util-is": "~1.0.0", 1009 | "inherits": "~2.0.3", 1010 | "isarray": "~1.0.0", 1011 | "process-nextick-args": "~2.0.0", 1012 | "safe-buffer": "~5.1.1", 1013 | "string_decoder": "~1.1.1", 1014 | "util-deprecate": "~1.0.1" 1015 | } 1016 | }, 1017 | "require-directory": { 1018 | "version": "2.1.1", 1019 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 1020 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" 1021 | }, 1022 | "require-main-filename": { 1023 | "version": "2.0.0", 1024 | "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", 1025 | "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" 1026 | }, 1027 | "safe-buffer": { 1028 | "version": "5.1.2", 1029 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1030 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 1031 | }, 1032 | "safer-buffer": { 1033 | "version": "2.1.2", 1034 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1035 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1036 | }, 1037 | "semver": { 1038 | "version": "5.7.1", 1039 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 1040 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" 1041 | }, 1042 | "set-blocking": { 1043 | "version": "2.0.0", 1044 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 1045 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" 1046 | }, 1047 | "setprototypeof": { 1048 | "version": "1.1.1", 1049 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 1050 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 1051 | }, 1052 | "signal-exit": { 1053 | "version": "3.0.3", 1054 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", 1055 | "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" 1056 | }, 1057 | "simple-concat": { 1058 | "version": "1.0.0", 1059 | "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", 1060 | "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" 1061 | }, 1062 | "simple-get": { 1063 | "version": "3.1.0", 1064 | "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", 1065 | "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", 1066 | "requires": { 1067 | "decompress-response": "^4.2.0", 1068 | "once": "^1.3.1", 1069 | "simple-concat": "^1.0.0" 1070 | } 1071 | }, 1072 | "sisteransi": { 1073 | "version": "1.0.5", 1074 | "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", 1075 | "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" 1076 | }, 1077 | "statuses": { 1078 | "version": "1.5.0", 1079 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 1080 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 1081 | }, 1082 | "string-width": { 1083 | "version": "1.0.2", 1084 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", 1085 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", 1086 | "requires": { 1087 | "code-point-at": "^1.0.0", 1088 | "is-fullwidth-code-point": "^1.0.0", 1089 | "strip-ansi": "^3.0.0" 1090 | } 1091 | }, 1092 | "string_decoder": { 1093 | "version": "1.1.1", 1094 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 1095 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 1096 | "requires": { 1097 | "safe-buffer": "~5.1.0" 1098 | } 1099 | }, 1100 | "strip-ansi": { 1101 | "version": "3.0.1", 1102 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 1103 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 1104 | "requires": { 1105 | "ansi-regex": "^2.0.0" 1106 | } 1107 | }, 1108 | "strip-json-comments": { 1109 | "version": "2.0.1", 1110 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 1111 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" 1112 | }, 1113 | "tar": { 1114 | "version": "4.4.10", 1115 | "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.10.tgz", 1116 | "integrity": "sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==", 1117 | "requires": { 1118 | "chownr": "^1.1.1", 1119 | "fs-minipass": "^1.2.5", 1120 | "minipass": "^2.3.5", 1121 | "minizlib": "^1.2.1", 1122 | "mkdirp": "^0.5.0", 1123 | "safe-buffer": "^5.1.2", 1124 | "yallist": "^3.0.3" 1125 | } 1126 | }, 1127 | "tar-fs": { 1128 | "version": "2.1.0", 1129 | "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.0.tgz", 1130 | "integrity": "sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg==", 1131 | "requires": { 1132 | "chownr": "^1.1.1", 1133 | "mkdirp-classic": "^0.5.2", 1134 | "pump": "^3.0.0", 1135 | "tar-stream": "^2.0.0" 1136 | } 1137 | }, 1138 | "tar-stream": { 1139 | "version": "2.1.2", 1140 | "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz", 1141 | "integrity": "sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==", 1142 | "requires": { 1143 | "bl": "^4.0.1", 1144 | "end-of-stream": "^1.4.1", 1145 | "fs-constants": "^1.0.0", 1146 | "inherits": "^2.0.3", 1147 | "readable-stream": "^3.1.1" 1148 | }, 1149 | "dependencies": { 1150 | "readable-stream": { 1151 | "version": "3.6.0", 1152 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 1153 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 1154 | "requires": { 1155 | "inherits": "^2.0.3", 1156 | "string_decoder": "^1.1.1", 1157 | "util-deprecate": "^1.0.1" 1158 | } 1159 | } 1160 | } 1161 | }, 1162 | "toidentifier": { 1163 | "version": "1.0.0", 1164 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 1165 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 1166 | }, 1167 | "tsscmp": { 1168 | "version": "1.0.6", 1169 | "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", 1170 | "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" 1171 | }, 1172 | "tunnel-agent": { 1173 | "version": "0.6.0", 1174 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 1175 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 1176 | "requires": { 1177 | "safe-buffer": "^5.0.1" 1178 | } 1179 | }, 1180 | "type-is": { 1181 | "version": "1.6.18", 1182 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1183 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1184 | "requires": { 1185 | "media-typer": "0.3.0", 1186 | "mime-types": "~2.1.24" 1187 | } 1188 | }, 1189 | "unpipe": { 1190 | "version": "1.0.0", 1191 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1192 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 1193 | }, 1194 | "util-deprecate": { 1195 | "version": "1.0.2", 1196 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1197 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 1198 | }, 1199 | "vary": { 1200 | "version": "1.1.2", 1201 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1202 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 1203 | }, 1204 | "which-module": { 1205 | "version": "2.0.0", 1206 | "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", 1207 | "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" 1208 | }, 1209 | "which-pm-runs": { 1210 | "version": "1.0.0", 1211 | "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", 1212 | "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=" 1213 | }, 1214 | "wide-align": { 1215 | "version": "1.1.3", 1216 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", 1217 | "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", 1218 | "requires": { 1219 | "string-width": "^1.0.2 || 2" 1220 | } 1221 | }, 1222 | "wrap-ansi": { 1223 | "version": "6.2.0", 1224 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", 1225 | "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", 1226 | "requires": { 1227 | "ansi-styles": "^4.0.0", 1228 | "string-width": "^4.1.0", 1229 | "strip-ansi": "^6.0.0" 1230 | }, 1231 | "dependencies": { 1232 | "ansi-regex": { 1233 | "version": "5.0.0", 1234 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", 1235 | "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" 1236 | }, 1237 | "is-fullwidth-code-point": { 1238 | "version": "3.0.0", 1239 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 1240 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 1241 | }, 1242 | "string-width": { 1243 | "version": "4.2.0", 1244 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", 1245 | "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", 1246 | "requires": { 1247 | "emoji-regex": "^8.0.0", 1248 | "is-fullwidth-code-point": "^3.0.0", 1249 | "strip-ansi": "^6.0.0" 1250 | } 1251 | }, 1252 | "strip-ansi": { 1253 | "version": "6.0.0", 1254 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", 1255 | "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", 1256 | "requires": { 1257 | "ansi-regex": "^5.0.0" 1258 | } 1259 | } 1260 | } 1261 | }, 1262 | "wrappy": { 1263 | "version": "1.0.2", 1264 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1265 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 1266 | }, 1267 | "y18n": { 1268 | "version": "4.0.0", 1269 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", 1270 | "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" 1271 | }, 1272 | "yallist": { 1273 | "version": "3.1.1", 1274 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", 1275 | "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" 1276 | }, 1277 | "yargs": { 1278 | "version": "15.3.1", 1279 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", 1280 | "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", 1281 | "requires": { 1282 | "cliui": "^6.0.0", 1283 | "decamelize": "^1.2.0", 1284 | "find-up": "^4.1.0", 1285 | "get-caller-file": "^2.0.1", 1286 | "require-directory": "^2.1.1", 1287 | "require-main-filename": "^2.0.0", 1288 | "set-blocking": "^2.0.0", 1289 | "string-width": "^4.2.0", 1290 | "which-module": "^2.0.0", 1291 | "y18n": "^4.0.0", 1292 | "yargs-parser": "^18.1.1" 1293 | }, 1294 | "dependencies": { 1295 | "ansi-regex": { 1296 | "version": "5.0.0", 1297 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", 1298 | "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" 1299 | }, 1300 | "is-fullwidth-code-point": { 1301 | "version": "3.0.0", 1302 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 1303 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 1304 | }, 1305 | "string-width": { 1306 | "version": "4.2.0", 1307 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", 1308 | "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", 1309 | "requires": { 1310 | "emoji-regex": "^8.0.0", 1311 | "is-fullwidth-code-point": "^3.0.0", 1312 | "strip-ansi": "^6.0.0" 1313 | } 1314 | }, 1315 | "strip-ansi": { 1316 | "version": "6.0.0", 1317 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", 1318 | "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", 1319 | "requires": { 1320 | "ansi-regex": "^5.0.0" 1321 | } 1322 | } 1323 | } 1324 | }, 1325 | "yargs-parser": { 1326 | "version": "18.1.3", 1327 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", 1328 | "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", 1329 | "requires": { 1330 | "camelcase": "^5.0.0", 1331 | "decamelize": "^1.2.0" 1332 | } 1333 | }, 1334 | "ylru": { 1335 | "version": "1.2.1", 1336 | "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", 1337 | "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==" 1338 | } 1339 | } 1340 | } 1341 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gd-utils", 3 | "version": "1.0.1", 4 | "description": "google drive utils", 5 | "repository": "iwestlin/gd-utils", 6 | "main": "src/gd.js", 7 | "scripts": { 8 | "start": "https_proxy='http://127.0.0.1:1086' nodemon server.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "viegg", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@koa/router": "^9.0.1", 16 | "@viegg/axios": "^1.0.0", 17 | "better-sqlite3": "^7.1.0", 18 | "bytes": "^3.1.0", 19 | "cli-table3": "^0.6.0", 20 | "colors": "^1.4.0", 21 | "dayjs": "^1.8.28", 22 | "gtoken": "^5.0.1", 23 | "html-escaper": "^3.0.0", 24 | "https-proxy-agent": "^5.0.0", 25 | "koa": "^2.13.0", 26 | "koa-bodyparser": "^4.3.0", 27 | "p-limit": "^3.0.1", 28 | "prompts": "^2.3.2", 29 | "proxy-agent": "^3.1.1", 30 | "signal-exit": "^3.0.3", 31 | "yargs": "^15.3.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Google Drive 百宝箱 2 | 3 | > [与其他工具的对比](./compare.md) 4 | 5 | ## 目录 6 | - [更新日志](#更新日志) 7 | - [demo](#demo) 8 | - [English Version (thanks to @roshanconnor123)](#english-version-thanks-to-roshanconnor123) 9 | - [无需域名和nginx版](#无需域名和nginx版) 10 | - [colab脚本(省去本地安装步骤,直接网页可用,感谢贡献者@orange2008)](#colab脚本省去本地安装步骤直接网页可用感谢贡献者orange2008) 11 | - [一键安装脚本(感谢 脚本制作者 @vitaminx)](#一键安装脚本感谢-脚本制作者-vitaminx) 12 | - [繁体中文版(感谢贡献者@liaojack8)](#繁体中文版感谢贡献者liaojack8) 13 | - [Docker 版(感谢贡献者@gdtool)](#docker-版感谢贡献者gdtool) 14 | - [常见问题](#常见问题) 15 | - [搭建过程](#搭建过程) 16 | - [功能简介](#功能简介) 17 | - [环境配置](#环境配置) 18 | - [依赖安装](#依赖安装) 19 | - [Service Account 配置](#service-account-配置) 20 | - [个人帐号配置](#个人帐号配置) 21 | - [Bot配置](#bot配置) 22 | - [补充说明](#补充说明) 23 | - [专家设置](#专家设置) 24 | - [注意事项](#注意事项) 25 | 26 | ## [更新日志](./changelog.md) 27 | 28 | ## demo 29 | [https://drive.google.com/drive/folders/124pjM5LggSuwI1n40bcD5tQ13wS0M6wg](https://drive.google.com/drive/folders/124pjM5LggSuwI1n40bcD5tQ13wS0M6wg) 30 | 31 | ## English Version (thanks to [@roshanconnor123](https://github.com/roshanconnor123)) 32 | [https://github.com/roshanconnor123/gd-utils](https://github.com/roshanconnor123/gd-utils) 33 | 34 | ## 无需域名和nginx版 35 | > 此版本无需域名和web服务即可使用tg机器人,大大简化了配置过程,感谢贡献者 [@dissipator](https://github.com/dissipator) 36 | 37 | [https://github.com/dissipator/gd-utils](https://github.com/dissipator/gd-utils) 38 | 39 | ## colab脚本(省去本地安装步骤,直接网页可用,感谢贡献者[@orange2008](https://github.com/orange2008)) 40 | [https://colab.research.google.com/drive/1i1W9nAzgiDtfA_rmTBcpMpwxVUhwgLsq](https://colab.research.google.com/drive/1i1W9nAzgiDtfA_rmTBcpMpwxVUhwgLsq) 41 | 42 | > 打开上面链接后,保存到自己的云端硬盘(请一定要保存,因为上面的共享链接操作记录所有人可见) 43 | 44 | colab使用录屏:[https://drive.google.com/drive/folders/19T37ARH7M1h67JGYanKp9LvORjJLEp_x](https://drive.google.com/drive/folders/19T37ARH7M1h67JGYanKp9LvORjJLEp_x) 45 | 46 | 这里还有另一位网友[@iErics](https://github.com/iErics)制作的colab脚本,界面更加规整,功能也更完整些(比如可以选择是否继续任务等),使用方法大同小异: 47 | [https://colab.research.google.com/github/iErics/gd-utils/blob/master/Colab_gd_utils.ipynb](https://colab.research.google.com/github/iErics/gd-utils/blob/master/Colab_gd_utils.ipynb) 48 | 49 | ## 一键安装脚本(感谢 脚本制作者 [@vitaminx](https://github.com/vitaminx)) 50 | > 如果你没有Linux操作经验或者是新开的vps,可尝试使用此脚本 51 | 52 | 请访问 [https://github.com/vitaminx/gd-utils](https://github.com/vitaminx/gd-utils) 获取安装方法 53 | 54 | ## 繁体中文版(感谢贡献者[@liaojack8](https://github.com/liaojack8/)) 55 | [https://github.com/liaojack8/gd-utils-cht](https://github.com/liaojack8/gd-utils-cht) 56 | 57 | > 目前项目处于起始阶段,尚不支持 i18n(多语言) ,所以上面繁体版是hard code的fork,如果你有兴趣让本项目增加多语言支持,欢迎PR。 58 | 59 | ## Docker 版(感谢贡献者[@gdtool](https://github.com/gdtool/)) 60 | [https://github.com/gdtool/gd-utils-docker](https://github.com/gdtool/gd-utils-docker) 61 | 62 | ## 常见问题 63 | **[如果你遇到任务完成时拷贝成功的文件少于统计的文件数,请务必点击查看](https://github.com/iwestlin/gd-utils/blob/master/changelog.md#%E9%87%8D%E8%A6%81%E6%9B%B4%E6%96%B02020-06-29)** 64 | 65 | 在命令行操作时有时会输出Google内部报错信息,这是正常情况,不会影响最终结果,因为程序对每个请求都有7次重试的机制。 66 | 如果经常出现404 file not found的错误,说明是sa的权限有问题,请点击上面的链接查看解决办法。 67 | 68 | 复制结束后,如果最后输出的消息里有 `未读取完毕的目录ID`,只需要在命令行执行上次同样的拷贝命令,选continue即可继续。 69 | 70 | 如果你复制完成以后,统计新的文件夹链接发现文件数比源文件夹少,说明Google正在更新数据库,请给它一点时间,一般等半小时再统计数据会比较完整。 71 | 72 | 如果你使用tg机器人拷贝文件数超多的目录时,发送拷贝命令以后,任务进度很久未开始,这是因为程序正在获取源文件夹的所有文件信息。 73 | 74 | **转存的运行机制严格按照以下顺序**: 75 | ``` 76 | 1、获取源文件夹所有文件信息 77 | 2、根据源文件夹的目录结构,在目标文件夹创建目录 78 | 3、所有目录创建完成后,开始复制文件 79 | ``` 80 | 81 | 如果源文件夹的文件数非常多(数十万),在命令行操作时需要添加额外参数:(因为程序运行的时候会把文件信息保存在内存中,文件数太多的话容易内存占用太多被nodejs干掉) 82 | ``` 83 | node --max-old-space-size=1024 count folder-id -S 84 | ``` 85 | 这样进程就能最大占用 1G 内存了,我最多测试过200万+文件数的任务,1G 内存足以完成。 86 | 87 | 这里还有一些网友的踩坑心得,如果你配置的时候也不小心掉进坑里,可以进去找找有没有解决办法: 88 | - [ikarosone 基于宝塔的搭建过程](https://www.ikarosone.top/archives/195.html) 89 | - [@greathappyforest 踩的坑](doc/tgbot-appache2-note.md) 90 | 91 | ## 搭建过程 92 | 机器人搭建过程录屏:[https://drive.google.com/drive/folders/1Lu7Cwh9lIJkfqYDIaJrFpzi8Lgdxr4zT](https://drive.google.com/drive/folders/1Lu7Cwh9lIJkfqYDIaJrFpzi8Lgdxr4zT) 93 | 94 | 需要注意的地方: 95 | 96 | - 视频中省略了一个比较重要的步骤就是**从本地上传service account授权文件到 sa 目录下**,tg机器人的所有操作默认都是通过sa授权的,所以读者请不要忘了 97 | - 视频中**nginx的配置里,server_name就是你的二级域名,需要和cloudflare的设置一样**的(mybbbottt),我分开录的视频所以没做到一致。 98 | - 还有省略的步骤就是注册域名和把域名托管到cloudflare了,这一步网上太多资料了,甚至也有免费注册(一年)域名的地方( https://www.freenom.com/ ),具体教程请自行搜索 99 | 100 | ## 功能简介 101 | 本工具目前支持以下功能: 102 | - 统计任意(您拥有相关权限的,下同,不再赘述)目录的文件信息,且支持以各种形式(html, tree, table, json)导出。 103 | 支持中断恢复,且统计过的目录(包括其所有递归子目录)信息会记录在本地数据库文件中(gdurl.sqlite) 104 | 请在本项目目录下命令行输入 `./count -h` 查看使用帮助 105 | 106 | - 拷贝任意目录所有文件到您指定目录,同样支持中断恢复。 107 | 支持根据文件大小过滤,可输入 `./copy -h` 查看使用帮助 108 | 109 | - 对任意目录进行去重,删除同一目录下的md5值相同的文件(只保留一个),删除同目录下的同名空目录。 110 | 命令行输入 `./dedupe -h` 查看使用帮助 111 | 112 | - 在 config.js 里完成相关配置后,可以将本项目部署在(可正常访问谷歌服务的)服务器上,提供 http 文件统计接口 113 | 114 | - 支持 telegram bot,配置完成后,上述功能大多可以通过 bot 进行操作 115 | 116 | ## 环境配置 117 | 本工具需要安装nodejs,客户端安装请访问[https://nodejs.org/zh-cn/download/](https://nodejs.org/zh-cn/download/),服务器安装可参考[https://github.com/nodesource/distributions/blob/master/README.md#debinstall](https://github.com/nodesource/distributions/blob/master/README.md#debinstall) 118 | 119 | 建议选择v12版本的node,以防接下来安装依赖出错。 120 | 121 | 如果你的网络环境无法正常访问谷歌服务,需要先在命令行进行一些配置:(如果可以正常访问则跳过此节) 122 | ``` 123 | http_proxy="YOUR_PROXY_URL" && https_proxy=$http_proxy && HTTP_PROXY=$http_proxy && HTTPS_PROXY=$http_proxy 124 | ``` 125 | 请把`YOUR_PROXY_URL`替换成你自己的代理地址 126 | 127 | ## 依赖安装 128 | - 命令行执行`git clone https://github.com/iwestlin/gd-utils && cd gd-utils` 克隆并切换到本项目文件夹下 129 | - **执行 `npm install --unsafe-perm=true --allow-root` 安装依赖**,部分依赖可能需要代理环境才能下载,所以需要上一步的配置 130 | 131 | 如果在安装过程中发生报错,请切换nodejs版本到v12再试。如果报错信息里有`Error: not found: make`之类的消息,说明你的命令行环境缺少make命令,可参考[这里](https://askubuntu.com/questions/192645/make-command-not-found)或直接google搜索`Make Command Not Found` 132 | 133 | 如果报错信息里有 `better-sqlite3`,先执行 `npm config set unsafe-perm=true` 134 | 然后 `rm -rf node_module` 删掉依赖目录,最后再执行下`npm i`安装试试。 135 | 136 | 依赖安装完成后,项目文件夹下会多出个`node_modules`目录,请不要删除它,接下来进行下一步配置。 137 | 138 | ## Service Account 配置 139 | 强烈建议使用service account(后称SA),因为机器人的所有操作默认都用的SA权限。 140 | SA授权文件获取方法请参见 141 | - 英文[https://github.com/xyou365/AutoRclone](https://github.com/xyou365/AutoRclone) 142 | - 中文[http://blog.jialezi.net/?post=153](http://blog.jialezi.net/?post=153) 143 | 144 | 获取到 SA 的 json 文件并将其加入团队盘成员后,请将文件拷贝到gd-utils的 `sa` 目录下。 145 | 注意,AutoRclone 将 SA 加入 group 的脚本有点问题,可能会加入不完全,而gd-utils混入未授权的SA文件会导致严重的问题,暂时的解决方法是[批量验证SA的有效性](https://github.com/iwestlin/gd-utils/blob/master/changelog.md#%E9%87%8D%E8%A6%81%E6%9B%B4%E6%96%B02020-06-29) 146 | 147 | 配置好 SA 以后,如果你不需要对个人盘下的文件进行操作,可跳过[个人帐号配置]这节,而且命令行执行命令的时候,记得带上 `-S` 参数告诉程序使用SA授权进行操作。 148 | 149 | ## 个人帐号配置 150 | - 命令行执行 `rclone config file` 找到 rclone 的配置文件路径 151 | - 打开这个配置文件 `rclone.conf`, 找到 `client_id`, `client_secret` 和 `refresh_token` 这三个变量,将其分别填入本项目下的 `config.js` 中,需要注意这三个值必须被成对的英文引号包裹,且引号后以英文逗号结尾,也就是需要符合JavaScript的[对象语法](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Object_initializer) 152 | 153 | 如果你没有配置过rclone,可以搜索`rclone google drive 教程`完成相关配置。 154 | 155 | 如果你的`rclone.conf`里没有`client_id`和`client_secret`,说明你配置rclone的时候默认用了rclone自己的client_id,连rclone自己[都不建议这样做](https://github.com/rclone/rclone/blob/8d55367a6a2f47a1be7e360a872bd7e56f4353df/docs/content/drive.md#making-your-own-client_id),因为大家共享了它的接口调用限额,在使用高峰期可能会触发限制。 156 | 157 | 获取自己的clinet_id可以参见这两篇文章:[Cloudbox/wiki/Google-Drive-API-Client-ID-and-Client-Secret](https://github.com/Cloudbox/Cloudbox/wiki/Google-Drive-API-Client-ID-and-Client-Secret) 和 [https://p3terx.com/archives/goindex-google-drive-directory-index.html#toc_2](https://p3terx.com/archives/goindex-google-drive-directory-index.html#toc_2) 158 | 159 | 获取到client_id和client_secret后,再次执行一遍`rclone config`,创建一个新的remote,**在配置过程中一定要填入你新获取的clinet_id和client_secret**,就能在`rclone.conf`里看到新获取的`refresh_token`了。**注意,不能使用之前的refrest_token**,因为它对应的是rclone自带的client_id 160 | 161 | 参数配置好以后,在命令行执行 `node check.js`,如果命令返回了你的谷歌硬盘根目录的数据,说明配置成功,可以开始使用本工具了。 162 | 163 | ## Bot配置 164 | 如果要使用 telegram bot 功能,需要进一步配置。 165 | 166 | 首先在 [https://core.telegram.org/bots#6-botfather](https://core.telegram.org/bots#6-botfather) 根据指示拿到 bot 的 token,然后填入 config.js 中的 `tg_token` 变量。 167 | 168 | 然后获取自己的 telegram username,这个username不是显示的名称,而是tg个人网址后面的那串字符,比如,我的tg个人网址是 `https://t.me/viegg` ,用户名就是 `viegg`,获取用户名的目的是在代码里配置白名单,只允许特定的用户调用机器人。将username填入 `config.js`里的配置,像这样: 169 | `tg_whitelist: ['viegg']`,就代表只允许我自己使用这个机器人了。 170 | 171 | 如果想把机器人的使用权限分享给别的用户,只需要改成: 172 | ``` 173 | tg_whitelist: ['viegg', '其他人的username'], 174 | ``` 175 | 176 | 接下来需要将代码部署到服务器上。 177 | 如果你一开始就是在服务器上配置的,可以直接执行`npm i pm2 -g` 178 | 179 | 如果你之前是在本地操作的,请在服务器上同样重复一遍,配置好相关参数后,执行`npm i pm2 -g`安装进程守护程序pm2 180 | 181 | 安装好pm2之后,执行 `pm2 start server.js --node-args="--max-old-space-size=1024"`,代码运行后会在服务器上监听`23333`端口。 182 | 183 | 如果你启动程序后想看运行日志,执行 `pm2 logs` 184 | 185 | 查看 pm2 守护的进程列表,执行 `pm2 l` 186 | 187 | 停止运行中的进程,执行 `pm2 stop 对应的进程名称` 188 | 189 | **如果你修改了代码中的配置,需要 `pm2 reload server` 才能生效**。 190 | 191 | > 如果你不想用nginx,可以将`server.js`中的`23333`改成`80`直接监听80端口(可能需要root权限) 192 | 193 | 接下来可通过nginx或其他工具起一个web服务,示例nginx配置: 194 | ``` 195 | server { 196 | listen 80; 197 | server_name your.server.name; 198 | 199 | location / { 200 | proxy_set_header Host $host; 201 | proxy_set_header X-Real-IP $remote_addr; 202 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 203 | proxy_pass http://127.0.0.1:23333/; 204 | } 205 | } 206 | ``` 207 | 配置好nginx后,可以再套一层cloudflare,具体教程请自行搜索。 208 | 209 | 检查网站是否部署成功,可以命令行执行(请将YOUR_WEBSITE_URL替换成你的网址) 210 | ``` 211 | curl 'YOUR_WEBSITE_URL/api/gdurl/count?fid=124pjM5LggSuwI1n40bcD5tQ13wS0M6wg' 212 | ``` 213 | 如果返回了`gd-utils 成功启动`的消息,说明部署成功了。 214 | 215 | 最后,在命令行执行(请将`YOUR_WEBSITE`和`YOUR_BOT_TOKEN`分别替换成你自己的网址和bot token) 216 | ``` 217 | curl -F "url=YOUR_WEBSITE/api/gdurl/tgbot" 'https://api.telegram.org/botYOUR_BOT_TOKEN/setWebhook' 218 | ``` 219 | 这样,就将你的服务器连接上你的 telegram bot 了,试着给bot发送个 `/help`,如果它回复给你使用说明,那就配置成功了。 220 | 221 | ## 补充说明 222 | 在`config.js`文件里,还有另外的几个参数: 223 | ``` 224 | // 单次请求多少毫秒未响应以后超时(基准值,若连续超时则下次调整为上次的2倍) 225 | const TIMEOUT_BASE = 7000 226 | 227 | // 最大超时设置,比如某次请求,第一次7s超时,第二次14s,第三次28s,第四次56s,第五次不是112s而是60s,后续同理 228 | const TIMEOUT_MAX = 60000 229 | 230 | const PAGE_SIZE = 1000 // 每次网络请求读取目录下的文件数,数值越大,越有可能超时,不得超过1000 231 | 232 | const RETRY_LIMIT = 7 // 如果某次请求失败,允许其重试的最大次数 233 | const PARALLEL_LIMIT = 20 // 单个任务的网络请求并行数量,可根据网络环境调整 234 | 235 | const DEFAULT_TARGET = '' // 必填,拷贝默认目的地ID,如果不指定target,则会拷贝到此处,建议填写团队盘ID,注意要用英文引号包裹 236 | ``` 237 | 读者可根据各自情况进行调整 238 | 239 | ## 专家设置 240 | 这一节面向更加注重安全的专家用户,并假设读者了解nodejs的基本语法 241 | 242 | 在 `config.js` 中,你可以额外设置两个变量 `ROUTER_PASSKEY` 和 `TG_IPLIST` 来进一步保证接口安全。 243 | ```javascript 244 | // 如果设置了这个值,那么调用 /api/gdurl/count 这个接口必须携带一个叫 passkey 的query,且必须等于ROUTER_PASSKEY的值 245 | // 如果不设置这个值,那么默认关闭 /api/gdurl/count 这个接口的功能(因为观察到很多用户公开的贴出了自己的API地址……) 246 | const ROUTER_PASSKEY = 'your-custom-passkey' 247 | 248 | // 与你的服务器通信的tg服务器的 ip 地址,可以在pm2 logs 中看到 249 | // 如果设置了这个值,那么调用 /api/gdurl/tgbot 这个接口的IP地址必须是 TG_IPLIST 数组的其中之一 250 | // 如果不设置这个值,则默认任何IP都可以调用此接口(考虑到后面还有个 tg username的白名单验证) 251 | const TG_IPLIST = ['tg-ip-address'] 252 | 253 | module.exports = { 254 | AUTH, 255 | PARALLEL_LIMIT, 256 | RETRY_LIMIT, 257 | TIMEOUT_BASE, 258 | TIMEOUT_MAX, 259 | LOG_DELAY, 260 | PAGE_SIZE, 261 | DEFAULT_TARGET, 262 | ROUTER_PASSKEY, 263 | TG_IPLIST 264 | } 265 | ``` 266 | 267 | ## 注意事项 268 | gd-utlis(以及所有GD转存工具)的原理是调用了[google drive官方接口](https://developers.google.com/drive/api/v3/reference/files/copy) 269 | 270 | gd-utils比较快的原因在[与其他工具的对比](./compare.md)有具体阐述,概括来讲,当它进行转存任务时,不会向google服务器查询目标文件是否已存在,因为它会把复制记录存储在本地数据库,这样就节省了查询花费的时间,而查询接口是google drive所有接口里最耗时的。 271 | 272 | 这也就导致了gd-utils目前无法对已存在的文件进行增量更新,**除非文件之前就是它拷贝的**,由于它已经将记录保存在本地,所以可以对之前的记录进行增量更新。 273 | 274 | 目前尚不知道google是否会对接口做频率限制,也不知道会不会影响google账号本身的安全。 275 | 276 | **请勿滥用,后果自负** 277 | -------------------------------------------------------------------------------- /sa/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErics/gd-utils/f4c8450a3f81ba65c7208031d9fdfbb2eeb7980c/sa/.keep -------------------------------------------------------------------------------- /sa/invalid/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErics/gd-utils/f4c8450a3f81ba65c7208031d9fdfbb2eeb7980c/sa/invalid/.keep -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const dayjs = require('dayjs') 2 | const Koa = require('koa') 3 | const bodyParser = require('koa-bodyparser') 4 | 5 | const router = require('./src/router') 6 | 7 | const app = new Koa() 8 | app.proxy = true 9 | 10 | app.use(catcher) 11 | app.use(bodyParser()) 12 | app.use(router.routes()) 13 | app.use(router.allowedMethods()) 14 | 15 | app.use(ctx => { 16 | ctx.status = 404 17 | ctx.body = 'not found' 18 | }) 19 | 20 | const PORT = 23333 21 | app.listen(PORT, '0.0.0.0', console.log('http://127.0.0.1:' + PORT)) 22 | 23 | async function catcher (ctx, next) { 24 | try { 25 | return await next() 26 | } catch (e) { 27 | console.error(e) 28 | ctx.status = 500 29 | ctx.body = e.message 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/gd.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const dayjs = require('dayjs') 4 | const prompts = require('prompts') 5 | const pLimit = require('p-limit') 6 | const axios = require('@viegg/axios') 7 | const { GoogleToken } = require('gtoken') 8 | const handle_exit = require('signal-exit') 9 | const { argv } = require('yargs') 10 | 11 | let { PARALLEL_LIMIT, EXCEED_LIMIT } = require('../config') 12 | PARALLEL_LIMIT = argv.l || argv.limit || PARALLEL_LIMIT 13 | EXCEED_LIMIT = EXCEED_LIMIT || 7 14 | 15 | const { AUTH, RETRY_LIMIT, TIMEOUT_BASE, TIMEOUT_MAX, LOG_DELAY, PAGE_SIZE, DEFAULT_TARGET } = require('../config') 16 | const { db } = require('../db') 17 | const { make_table, make_tg_table, make_html, summary } = require('./summary') 18 | const { gen_tree_html } = require('./tree') 19 | 20 | const FILE_EXCEED_MSG = '您的团队盘文件数已超限(40万),停止复制。请将未复制完成的文件夹(或者它的任意子文件夹)移到另一个(sa也有权限的)团队盘中,再执行一遍复制指令即可接上进度继续复制(是的你没看错...)' 21 | const FOLDER_TYPE = 'application/vnd.google-apps.folder' 22 | const sleep = ms => new Promise((resolve, reject) => setTimeout(resolve, ms)) 23 | 24 | const { https_proxy, http_proxy, all_proxy } = process.env 25 | const proxy_url = https_proxy || http_proxy || all_proxy 26 | 27 | let axins 28 | if (proxy_url) { 29 | console.log('使用代理:', proxy_url) 30 | let ProxyAgent 31 | try { 32 | ProxyAgent = require('proxy-agent') 33 | } catch (e) { // 没执行 npm i proxy-agent 34 | ProxyAgent = require('https-proxy-agent') 35 | } 36 | axins = axios.create({ httpsAgent: new ProxyAgent(proxy_url) }) 37 | } else { 38 | axins = axios.create({}) 39 | } 40 | 41 | const SA_LOCATION = argv.sa || 'sa' 42 | const SA_BATCH_SIZE = 1000 43 | const SA_FILES = fs.readdirSync(path.join(__dirname, '..', SA_LOCATION)).filter(v => v.endsWith('.json')) 44 | SA_FILES.flag = 0 45 | let SA_TOKENS = get_sa_batch() 46 | 47 | if (is_pm2()) { 48 | setInterval(() => { 49 | SA_FILES.flag = 0 50 | SA_TOKENS = get_sa_batch() 51 | }, 1000 * 3600 * 2) 52 | } 53 | 54 | // https://github.com/Leelow/is-pm2/blob/master/index.js 55 | function is_pm2 () { 56 | return 'PM2_HOME' in process.env || 'PM2_JSON_PROCESSING' in process.env || 'PM2_CLI' in process.env 57 | } 58 | 59 | function get_sa_batch () { 60 | const new_flag = SA_FILES.flag + SA_BATCH_SIZE 61 | const files = SA_FILES.slice(SA_FILES.flag, new_flag) 62 | SA_FILES.flag = new_flag 63 | return files.map(filename => { 64 | const gtoken = new GoogleToken({ 65 | keyFile: path.join(__dirname, '..', SA_LOCATION, filename), 66 | scope: ['https://www.googleapis.com/auth/drive'] 67 | }) 68 | return { gtoken, expires: 0 } 69 | }) 70 | } 71 | 72 | handle_exit(() => { 73 | // console.log('handle_exit running') 74 | const records = db.prepare('select id from task where status=?').all('copying') 75 | records.forEach(v => { 76 | db.prepare('update task set status=? where id=?').run('interrupt', v.id) 77 | }) 78 | records.length && console.log(records.length, 'task interrupted') 79 | db.close() 80 | }) 81 | 82 | async function gen_count_body ({ fid, type, update, service_account }) { 83 | async function update_info () { 84 | const info = await walk_and_save({ fid, update, service_account }) // 这一步已经将fid记录存入数据库中了 85 | const row = db.prepare('SELECT summary from gd WHERE fid=?').get(fid) 86 | if (!row) return [] 87 | return [info, JSON.parse(row.summary)] 88 | } 89 | 90 | function render_smy (smy, type) { 91 | if (!smy) return 92 | if (['html', 'curl', 'tg'].includes(type)) { 93 | smy = (typeof smy === 'object') ? smy : JSON.parse(smy) 94 | const type_func = { 95 | html: make_html, 96 | curl: make_table, 97 | tg: make_tg_table 98 | } 99 | return type_func[type](smy) 100 | } else { // 默认输出json 101 | return (typeof smy === 'string') ? smy : JSON.stringify(smy) 102 | } 103 | } 104 | const file = await get_info_by_id(fid, service_account) 105 | if (file && file.mimeType !== FOLDER_TYPE) return render_smy(summary([file]), type) 106 | 107 | let info, smy 108 | const record = db.prepare('SELECT * FROM gd WHERE fid = ?').get(fid) 109 | if (!record || update) { 110 | [info, smy] = await update_info() 111 | } 112 | if (type === 'all') { 113 | info = info || get_all_by_fid(fid) 114 | if (!info) { // 说明上次统计过程中断了 115 | [info] = await update_info() 116 | } 117 | return info && JSON.stringify(info) 118 | } 119 | if (smy) return render_smy(smy, type) 120 | if (record && record.summary) return render_smy(record.summary, type) 121 | info = info || get_all_by_fid(fid) 122 | if (info) { 123 | smy = summary(info) 124 | } else { 125 | [info, smy] = await update_info() 126 | } 127 | return render_smy(smy, type) 128 | } 129 | 130 | async function count ({ fid, update, sort, type, output, not_teamdrive, service_account }) { 131 | sort = (sort || '').toLowerCase() 132 | type = (type || '').toLowerCase() 133 | output = (output || '').toLowerCase() 134 | if (!update) { 135 | const info = get_all_by_fid(fid) 136 | if (info) { 137 | console.log('找到本地缓存数据,缓存时间:', dayjs(info.mtime).format('YYYY-MM-DD HH:mm:ss')) 138 | const out_str = get_out_str({ info, type, sort }) 139 | if (output) return fs.writeFileSync(output, out_str) 140 | return console.log(out_str) 141 | } 142 | } 143 | const result = await walk_and_save({ fid, not_teamdrive, update, service_account }) 144 | const out_str = get_out_str({ info: result, type, sort }) 145 | if (output) { 146 | fs.writeFileSync(output, out_str) 147 | } else { 148 | console.log(out_str) 149 | } 150 | } 151 | 152 | function get_out_str ({ info, type, sort }) { 153 | const smy = summary(info, sort) 154 | let out_str 155 | if (type === 'tree') { 156 | out_str = gen_tree_html(info) 157 | } else if (type === 'html') { 158 | out_str = make_html(smy) 159 | } else if (type === 'json') { 160 | out_str = JSON.stringify(smy) 161 | } else if (type === 'all') { 162 | out_str = JSON.stringify(info) 163 | } else { 164 | out_str = make_table(smy) 165 | } 166 | return out_str 167 | } 168 | 169 | function get_all_by_fid (fid) { 170 | const record = db.prepare('SELECT * FROM gd WHERE fid = ?').get(fid) 171 | if (!record) return null 172 | const { info, subf } = record 173 | let result = JSON.parse(info) 174 | result = result.map(v => { 175 | v.parent = fid 176 | return v 177 | }) 178 | if (!subf) return result 179 | return recur(result, JSON.parse(subf)) 180 | 181 | function recur (result, subf) { 182 | if (!subf.length) return result 183 | const arr = subf.map(v => { 184 | const row = db.prepare('SELECT * FROM gd WHERE fid = ?').get(v) 185 | if (!row) return null // 如果没找到对应的fid记录,说明上次中断了进程或目录读取未完成 186 | let info = JSON.parse(row.info) 187 | info = info.map(vv => { 188 | vv.parent = v 189 | return vv 190 | }) 191 | return { info, subf: JSON.parse(row.subf) } 192 | }) 193 | if (arr.some(v => v === null)) return null 194 | const sub_subf = [].concat(...arr.map(v => v.subf).filter(v => v)) 195 | result = result.concat(...arr.map(v => v.info)) 196 | return recur(result, sub_subf) 197 | } 198 | } 199 | 200 | async function walk_and_save ({ fid, not_teamdrive, update, service_account }) { 201 | let result = [] 202 | const not_finished = [] 203 | const limit = pLimit(PARALLEL_LIMIT) 204 | 205 | const loop = setInterval(() => { 206 | const now = dayjs().format('HH:mm:ss') 207 | const message = `${now} | 已获取对象 ${result.length} | 网络请求 进行中${limit.activeCount}/排队中${limit.pendingCount}\n` 208 | print_progress(message) 209 | }, 3000) 210 | 211 | async function recur (parent) { 212 | let files, should_save 213 | if (update) { 214 | files = await limit(() => ls_folder({ fid: parent, not_teamdrive, service_account })) 215 | should_save = true 216 | } else { 217 | const record = db.prepare('SELECT * FROM gd WHERE fid = ?').get(parent) 218 | if (record) { 219 | files = JSON.parse(record.info) 220 | } else { 221 | files = await limit(() => ls_folder({ fid: parent, not_teamdrive, service_account })) 222 | should_save = true 223 | } 224 | } 225 | if (!files) return 226 | if (files.not_finished) not_finished.push(parent) 227 | should_save && save_files_to_db(parent, files) 228 | const folders = files.filter(v => v.mimeType === FOLDER_TYPE) 229 | files.forEach(v => v.parent = parent) 230 | result = result.concat(files) 231 | return Promise.all(folders.map(v => recur(v.id))) 232 | } 233 | try { 234 | await recur(fid) 235 | } catch (e) { 236 | console.error(e) 237 | } 238 | console.log('\n信息获取完毕') 239 | not_finished.length ? console.log('未读取完毕的目录ID:', JSON.stringify(not_finished)) : console.log('所有目录读取完毕') 240 | clearInterval(loop) 241 | const smy = summary(result) 242 | db.prepare('UPDATE gd SET summary=?, mtime=? WHERE fid=?').run(JSON.stringify(smy), Date.now(), fid) 243 | return result 244 | } 245 | 246 | function save_files_to_db (fid, files) { 247 | // 不保存请求未完成的目录,那么下次调用get_all_by_id会返回null,从而再次调用walk_and_save试图完成此目录的请求 248 | if (files.not_finished) return 249 | let subf = files.filter(v => v.mimeType === FOLDER_TYPE).map(v => v.id) 250 | subf = subf.length ? JSON.stringify(subf) : null 251 | const exists = db.prepare('SELECT fid FROM gd WHERE fid = ?').get(fid) 252 | if (exists) { 253 | db.prepare('UPDATE gd SET info=?, subf=?, mtime=? WHERE fid=?') 254 | .run(JSON.stringify(files), subf, Date.now(), fid) 255 | } else { 256 | db.prepare('INSERT INTO gd (fid, info, subf, ctime) VALUES (?, ?, ?, ?)') 257 | .run(fid, JSON.stringify(files), subf, Date.now()) 258 | } 259 | } 260 | 261 | async function ls_folder ({ fid, not_teamdrive, service_account }) { 262 | let files = [] 263 | let pageToken 264 | const search_all = { includeItemsFromAllDrives: true, supportsAllDrives: true } 265 | const params = ((fid === 'root') || not_teamdrive) ? {} : search_all 266 | params.q = `'${fid}' in parents and trashed = false` 267 | params.orderBy = 'folder,name desc' 268 | params.fields = 'nextPageToken, files(id, name, mimeType, size, md5Checksum)' 269 | params.pageSize = Math.min(PAGE_SIZE, 1000) 270 | // const use_sa = (fid !== 'root') && (service_account || !not_teamdrive) // 不带参数默认使用sa 271 | const use_sa = (fid !== 'root') && service_account 272 | // const headers = await gen_headers(use_sa) 273 | // 对于直接子文件数超多的目录(1ctMwpIaBg8S1lrZDxdynLXJpMsm5guAl),可能还没列完,access_token就过期了 274 | // 由于需要nextPageToken才能获取下一页的数据,所以无法用并行请求,测试发现每次获取1000个文件的请求大多需要20秒以上才能完成 275 | const gtoken = use_sa && (await get_sa_token()).gtoken 276 | do { 277 | if (pageToken) params.pageToken = pageToken 278 | let url = 'https://www.googleapis.com/drive/v3/files' 279 | url += '?' + params_to_query(params) 280 | let retry = 0 281 | let data 282 | const payload = { timeout: TIMEOUT_BASE } 283 | while (!data && (retry < RETRY_LIMIT)) { 284 | const access_token = gtoken ? (await gtoken.getToken()).access_token : (await get_access_token()) 285 | const headers = { authorization: 'Bearer ' + access_token } 286 | payload.headers = headers 287 | try { 288 | data = (await axins.get(url, payload)).data 289 | } catch (err) { 290 | handle_error(err) 291 | retry++ 292 | payload.timeout = Math.min(payload.timeout * 2, TIMEOUT_MAX) 293 | } 294 | } 295 | if (!data) { 296 | console.error('读取目录未完成(部分读取), 参数:', params) 297 | files.not_finished = true 298 | return files 299 | } 300 | files = files.concat(data.files) 301 | argv.sfl && console.log('files.length:', files.length) 302 | pageToken = data.nextPageToken 303 | } while (pageToken) 304 | 305 | return files 306 | } 307 | 308 | async function gen_headers (use_sa) { 309 | // use_sa = use_sa && SA_TOKENS.length 310 | const access_token = use_sa ? (await get_sa_token()).access_token : (await get_access_token()) 311 | return { authorization: 'Bearer ' + access_token } 312 | } 313 | 314 | function params_to_query (data) { 315 | const ret = [] 316 | for (let d in data) { 317 | ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d])) 318 | } 319 | return ret.join('&') 320 | } 321 | 322 | async function get_access_token () { 323 | const { expires, access_token, client_id, client_secret, refresh_token } = AUTH 324 | if (expires > Date.now()) return access_token 325 | 326 | const url = 'https://www.googleapis.com/oauth2/v4/token' 327 | const headers = { 'Content-Type': 'application/x-www-form-urlencoded' } 328 | const config = { headers } 329 | const params = { client_id, client_secret, refresh_token, grant_type: 'refresh_token' } 330 | const { data } = await axins.post(url, params_to_query(params), config) 331 | // console.log('Got new token:', data) 332 | AUTH.access_token = data.access_token 333 | AUTH.expires = Date.now() + 1000 * data.expires_in 334 | return data.access_token 335 | } 336 | 337 | // get_sa_token().catch(console.error) 338 | async function get_sa_token () { 339 | if (!SA_TOKENS.length) SA_TOKENS = get_sa_batch() 340 | while (SA_TOKENS.length) { 341 | const tk = get_random_element(SA_TOKENS) 342 | try { 343 | return await real_get_sa_token(tk) 344 | } catch (e) { 345 | console.warn('SA获取access_token失败:', e.message) 346 | SA_TOKENS = SA_TOKENS.filter(v => v.gtoken !== tk.gtoken) 347 | if (!SA_TOKENS.length) SA_TOKENS = get_sa_batch() 348 | } 349 | } 350 | throw new Error('没有可用的SA') 351 | } 352 | 353 | async function real_get_sa_token (el) { 354 | const { value, expires, gtoken } = el 355 | // 把gtoken传递出去的原因是当某账号流量用尽时可以依此过滤 356 | if (Date.now() < expires) return { access_token: value, gtoken } 357 | const { access_token, expires_in } = await gtoken.getToken({ forceRefresh: true }) 358 | el.value = access_token 359 | el.expires = Date.now() + 1000 * (expires_in - 60 * 5) // 提前5分钟判定为过期 360 | return { access_token, gtoken } 361 | } 362 | 363 | function get_random_element (arr) { 364 | return arr[~~(arr.length * Math.random())] 365 | } 366 | 367 | function validate_fid (fid) { 368 | if (!fid) return false 369 | fid = String(fid) 370 | const whitelist = ['root', 'appDataFolder', 'photos'] 371 | if (whitelist.includes(fid)) return true 372 | if (fid.length < 10 || fid.length > 100) return false 373 | const reg = /^[a-zA-Z0-9_-]+$/ 374 | return fid.match(reg) 375 | } 376 | 377 | async function create_folder (name, parent, use_sa, limit) { 378 | let url = `https://www.googleapis.com/drive/v3/files` 379 | const params = { supportsAllDrives: true } 380 | url += '?' + params_to_query(params) 381 | const post_data = { 382 | name, 383 | mimeType: FOLDER_TYPE, 384 | parents: [parent] 385 | } 386 | let retry = 0 387 | let err_message 388 | while (retry < RETRY_LIMIT) { 389 | try { 390 | const headers = await gen_headers(use_sa) 391 | return (await axins.post(url, post_data, { headers })).data 392 | } catch (err) { 393 | err_message = err.message 394 | retry++ 395 | handle_error(err) 396 | const data = err && err.response && err.response.data 397 | const message = data && data.error && data.error.message 398 | if (message && message.toLowerCase().includes('file limit')) { 399 | if (limit) limit.clearQueue() 400 | throw new Error(FILE_EXCEED_MSG) 401 | } 402 | console.log('创建目录重试中:', name, '重试次数:', retry) 403 | } 404 | } 405 | throw new Error(err_message + ' 目录名:' + name) 406 | } 407 | 408 | async function get_name_by_id (fid, use_sa) { 409 | try { 410 | const { name } = await get_info_by_id(fid, use_sa) 411 | return name 412 | } catch (e) { 413 | return fid 414 | } 415 | } 416 | 417 | async function get_info_by_id (fid, use_sa) { 418 | let url = `https://www.googleapis.com/drive/v3/files/${fid}` 419 | let params = { 420 | includeItemsFromAllDrives: true, 421 | supportsAllDrives: true, 422 | corpora: 'allDrives', 423 | fields: 'id, name, size, parents, mimeType' 424 | } 425 | url += '?' + params_to_query(params) 426 | const headers = await gen_headers(use_sa) 427 | const { data } = await axins.get(url, { headers }) 428 | return data 429 | } 430 | 431 | async function user_choose () { 432 | const response = await prompts({ 433 | type: 'number', 434 | name: 'value', 435 | message: `请输入选择(1/2/3): 436 | 1、继续 437 | 2、重来 438 | 3、退出 439 | `, 440 | validate: value => [1, 2, 3].includes(value) ? true : `必须输入 1/2/3` 441 | }) 442 | const choices = ['', 'continue', 'restart', 'exit'] 443 | return choices[response.value] 444 | } 445 | 446 | //async function user_choose () { 447 | // const answer = await prompts({ 448 | // type: 'select', 449 | // name: 'value', 450 | // message: '检测到上次的复制记录,是否继续?', 451 | // choices: [ 452 | // { title: 'Continue', description: '从上次中断的地方继续', value: 'continue' }, 453 | // { title: 'Restart', description: '无视已存在的记录,重新复制', value: 'restart' }, 454 | // { title: 'Exit', description: '直接退出', value: 'exit' } 455 | // ], 456 | // initial: 0 457 | // }) 458 | // return answer.value 459 | //} 460 | 461 | async function copy ({ source, target, name, min_size, update, not_teamdrive, service_account, dncnr, is_server }) { 462 | target = target || DEFAULT_TARGET 463 | if (!target) throw new Error('目标位置不能为空') 464 | 465 | const record = db.prepare('select id, status from task where source=? and target=?').get(source, target) 466 | if (record && record.status === 'copying') return console.log('已有相同源和目的地的任务正在运行,强制退出') 467 | 468 | try { 469 | return await real_copy({ source, target, name, min_size, update, dncnr, not_teamdrive, service_account, is_server }) 470 | } catch (err) { 471 | console.error('复制文件夹出错', err) 472 | const record = db.prepare('select id, status from task where source=? and target=?').get(source, target) 473 | if (record) db.prepare('update task set status=? where id=?').run('error', record.id) 474 | } 475 | } 476 | 477 | // 待解决:如果用户手动ctrl+c中断进程,那么已经发出的请求,就算完成了也不会记录到本地数据库中,所以可能产生重复文件(夹) 478 | async function real_copy ({ source, target, name, min_size, update, dncnr, not_teamdrive, service_account, is_server }) { 479 | async function get_new_root () { 480 | if (dncnr) return { id: target } 481 | if (name) { 482 | return create_folder(name, target, service_account) 483 | } else { 484 | const source_info = await get_info_by_id(source, service_account) 485 | return create_folder(source_info.name, target, service_account) 486 | } 487 | } 488 | 489 | const record = db.prepare('select * from task where source=? and target=?').get(source, target) 490 | if (record) { 491 | const copied = db.prepare('select fileid from copied where taskid=?').all(record.id).map(v => v.fileid) 492 | const choice = (is_server || argv.yes) ? 'continue' : await user_choose() 493 | if (choice === 'exit') { 494 | return console.log('退出程序') 495 | } else if (choice === 'continue') { 496 | let { mapping } = record 497 | const old_mapping = {} 498 | const copied_ids = {} 499 | copied.forEach(id => copied_ids[id] = true) 500 | mapping = mapping.trim().split('\n').map(line => line.split(' ')) 501 | const root = mapping[0][1] 502 | mapping.forEach(arr => old_mapping[arr[0]] = arr[1]) 503 | db.prepare('update task set status=? where id=?').run('copying', record.id) 504 | const arr = await walk_and_save({ fid: source, update, not_teamdrive, service_account }) 505 | let files = arr.filter(v => v.mimeType !== FOLDER_TYPE).filter(v => !copied_ids[v.id]) 506 | if (min_size) files = files.filter(v => v.size >= min_size) 507 | const folders = arr.filter(v => v.mimeType === FOLDER_TYPE) 508 | const all_mapping = await create_folders({ 509 | old_mapping, 510 | source, 511 | folders, 512 | service_account, 513 | root, 514 | task_id: record.id 515 | }) 516 | await copy_files({ files, service_account, root, mapping: all_mapping, task_id: record.id }) 517 | db.prepare('update task set status=?, ftime=? where id=?').run('finished', Date.now(), record.id) 518 | return { id: root, task_id: record.id } 519 | } else if (choice === 'restart') { 520 | const new_root = await get_new_root() 521 | const root_mapping = source + ' ' + new_root.id + '\n' 522 | db.prepare('update task set status=?, mapping=? where id=?').run('copying', root_mapping, record.id) 523 | db.prepare('delete from copied where taskid=?').run(record.id) 524 | // const arr = await walk_and_save({ fid: source, update: true, not_teamdrive, service_account }) 525 | const arr = await walk_and_save({ fid: source, update, not_teamdrive, service_account }) 526 | 527 | let files = arr.filter(v => v.mimeType !== FOLDER_TYPE) 528 | if (min_size) files = files.filter(v => v.size >= min_size) 529 | const folders = arr.filter(v => v.mimeType === FOLDER_TYPE) 530 | console.log('待复制的目录数:', folders.length) 531 | console.log('待复制的文件数:', files.length) 532 | const mapping = await create_folders({ 533 | source, 534 | folders, 535 | service_account, 536 | root: new_root.id, 537 | task_id: record.id 538 | }) 539 | await copy_files({ files, mapping, service_account, root: new_root.id, task_id: record.id }) 540 | db.prepare('update task set status=?, ftime=? where id=?').run('finished', Date.now(), record.id) 541 | return { id: new_root.id, task_id: record.id } 542 | } else { 543 | // ctrl+c 退出 544 | return console.log('退出程序') 545 | } 546 | } else { 547 | const new_root = await get_new_root() 548 | const root_mapping = source + ' ' + new_root.id + '\n' 549 | const { lastInsertRowid } = db.prepare('insert into task (source, target, status, mapping, ctime) values (?, ?, ?, ?, ?)').run(source, target, 'copying', root_mapping, Date.now()) 550 | const arr = await walk_and_save({ fid: source, update, not_teamdrive, service_account }) 551 | let files = arr.filter(v => v.mimeType !== FOLDER_TYPE) 552 | if (min_size) files = files.filter(v => v.size >= min_size) 553 | const folders = arr.filter(v => v.mimeType === FOLDER_TYPE) 554 | console.log('待复制的目录数:', folders.length) 555 | console.log('待复制的文件数:', files.length) 556 | const mapping = await create_folders({ 557 | source, 558 | folders, 559 | service_account, 560 | root: new_root.id, 561 | task_id: lastInsertRowid 562 | }) 563 | await copy_files({ files, mapping, service_account, root: new_root.id, task_id: lastInsertRowid }) 564 | db.prepare('update task set status=?, ftime=? where id=?').run('finished', Date.now(), lastInsertRowid) 565 | return { id: new_root.id, task_id: lastInsertRowid } 566 | } 567 | } 568 | 569 | async function copy_files ({ files, mapping, service_account, root, task_id }) { 570 | if (!files.length) return 571 | console.log('\n开始复制文件,总数:', files.length) 572 | 573 | const loop = setInterval(() => { 574 | const now = dayjs().format('HH:mm:ss') 575 | const message = `${now} | 已复制文件数 ${count} | 进行中 ${concurrency} | 排队中文件数 ${files.length}\n` 576 | print_progress(message) 577 | }, 3000) 578 | 579 | let count = 0 580 | let concurrency = 0 581 | let err 582 | do { 583 | if (err) { 584 | clearInterval(loop) 585 | files = null 586 | throw err 587 | } 588 | if (concurrency > PARALLEL_LIMIT) { 589 | await sleep(100) 590 | continue 591 | } 592 | const file = files.shift() 593 | if (!file) { 594 | await sleep(1000) 595 | continue 596 | } 597 | concurrency++ 598 | const { id, parent } = file 599 | const target = mapping[parent] || root 600 | copy_file(id, target, service_account, null, task_id).then(new_file => { 601 | if (new_file) { 602 | count++ 603 | db.prepare('INSERT INTO copied (taskid, fileid) VALUES (?, ?)').run(task_id, id) 604 | } 605 | }).catch(e => { 606 | err = e 607 | }).finally(() => { 608 | concurrency-- 609 | }) 610 | } while (concurrency || files.length) 611 | return clearInterval(loop) 612 | // const limit = pLimit(PARALLEL_LIMIT) 613 | // let count = 0 614 | // const loop = setInterval(() => { 615 | // const now = dayjs().format('HH:mm:ss') 616 | // const {activeCount, pendingCount} = limit 617 | // const message = `${now} | 已复制文件数 ${count} | 网络请求 进行中${activeCount}/排队中${pendingCount}` 618 | // print_progress(message) 619 | // }, 1000) 620 | // return Promise.all(files.map(async file => { 621 | // const { id, parent } = file 622 | // const target = mapping[parent] || root 623 | // const new_file = await limit(() => copy_file(id, target, service_account, limit, task_id)) 624 | // if (new_file) { 625 | // count++ 626 | // db.prepare('INSERT INTO copied (taskid, fileid) VALUES (?, ?)').run(task_id, id) 627 | // } 628 | // })).finally(() => clearInterval(loop)) 629 | } 630 | 631 | async function copy_file (id, parent, use_sa, limit, task_id) { 632 | let url = `https://www.googleapis.com/drive/v3/files/${id}/copy` 633 | let params = { supportsAllDrives: true } 634 | url += '?' + params_to_query(params) 635 | const config = {} 636 | let retry = 0 637 | while (retry < RETRY_LIMIT) { 638 | let gtoken 639 | if (use_sa) { 640 | const temp = await get_sa_token() 641 | gtoken = temp.gtoken 642 | config.headers = { authorization: 'Bearer ' + temp.access_token } 643 | } else { 644 | config.headers = await gen_headers() 645 | } 646 | try { 647 | const { data } = await axins.post(url, { parents: [parent] }, config) 648 | if (gtoken) gtoken.exceed_count = 0 649 | return data 650 | } catch (err) { 651 | retry++ 652 | handle_error(err) 653 | const data = err && err.response && err.response.data 654 | const message = data && data.error && data.error.message 655 | if (message && message.toLowerCase().includes('file limit')) { 656 | if (limit) limit.clearQueue() 657 | if (task_id) db.prepare('update task set status=? where id=?').run('error', task_id) 658 | throw new Error(FILE_EXCEED_MSG) 659 | } 660 | if (use_sa && message && message.toLowerCase().includes('rate limit')) { 661 | retry-- 662 | if (gtoken.exceed_count >= EXCEED_LIMIT) { 663 | SA_TOKENS = SA_TOKENS.filter(v => v.gtoken !== gtoken) 664 | if (!SA_TOKENS.length) SA_TOKENS = get_sa_batch() 665 | console.log(`此帐号连续${EXCEED_LIMIT}次触发使用限额,本批次剩余可用SA数量:`, SA_TOKENS.length) 666 | } else { 667 | // console.log('此帐号触发使用限额,已标记,若下次请求正常则解除标记,否则剔除此SA') 668 | if (gtoken.exceed_count) { 669 | gtoken.exceed_count++ 670 | } else { 671 | gtoken.exceed_count = 1 672 | } 673 | } 674 | } 675 | } 676 | } 677 | if (use_sa && !SA_TOKENS.length) { 678 | if (limit) limit.clearQueue() 679 | if (task_id) db.prepare('update task set status=? where id=?').run('error', task_id) 680 | throw new Error('没有可用的SA') 681 | } else { 682 | console.warn('复制文件失败,文件id: ' + id) 683 | } 684 | } 685 | 686 | async function create_folders ({ source, old_mapping, folders, root, task_id, service_account }) { 687 | if (argv.dncf) return {} // do not copy folders 688 | if (!Array.isArray(folders)) throw new Error('folders must be Array:' + folders) 689 | const mapping = old_mapping || {} 690 | mapping[source] = root 691 | if (!folders.length) return mapping 692 | 693 | const missed_folders = folders.filter(v => !mapping[v.id]) 694 | console.log('开始复制文件夹,总数:', missed_folders.length) 695 | const limit = pLimit(PARALLEL_LIMIT) 696 | let count = 0 697 | let same_levels = folders.filter(v => v.parent === folders[0].parent) 698 | 699 | const loop = setInterval(() => { 700 | const now = dayjs().format('HH:mm:ss') 701 | const message = `${now} | 已创建目录 ${count} | 网络请求 进行中${limit.activeCount}/排队中${limit.pendingCount}\n` 702 | print_progress(message) 703 | }, 3000) 704 | 705 | while (same_levels.length) { 706 | const same_levels_missed = same_levels.filter(v => !mapping[v.id]) 707 | await Promise.all(same_levels_missed.map(async v => { 708 | try { 709 | const { name, id, parent } = v 710 | const target = mapping[parent] || root 711 | const new_folder = await limit(() => create_folder(name, target, service_account, limit)) 712 | count++ 713 | mapping[id] = new_folder.id 714 | const mapping_record = id + ' ' + new_folder.id + '\n' 715 | db.prepare('update task set mapping = mapping || ? where id=?').run(mapping_record, task_id) 716 | } catch (e) { 717 | if (e.message === FILE_EXCEED_MSG) { 718 | clearInterval(loop) 719 | throw new Error(FILE_EXCEED_MSG) 720 | } 721 | console.error('创建目录出错:', e.message) 722 | } 723 | })) 724 | // folders = folders.filter(v => !mapping[v.id]) 725 | same_levels = [].concat(...same_levels.map(v => folders.filter(vv => vv.parent === v.id))) 726 | } 727 | 728 | clearInterval(loop) 729 | return mapping 730 | } 731 | 732 | function find_dupe (arr) { 733 | const files = arr.filter(v => v.mimeType !== FOLDER_TYPE) 734 | const folders = arr.filter(v => v.mimeType === FOLDER_TYPE) 735 | const exists = {} 736 | const dupe_files = [] 737 | const dupe_folder_keys = {} 738 | for (const folder of folders) { 739 | const { parent, name } = folder 740 | const key = parent + '|' + name 741 | if (exists[key]) { 742 | dupe_folder_keys[key] = true 743 | } else { 744 | exists[key] = true 745 | } 746 | } 747 | const dupe_empty_folders = folders.filter(folder => { 748 | const { parent, name } = folder 749 | const key = parent + '|' + name 750 | return dupe_folder_keys[key] 751 | }).filter(folder => { 752 | const has_child = arr.some(v => v.parent === folder.id) 753 | return !has_child 754 | }) 755 | for (const file of files) { 756 | const { md5Checksum, parent, name } = file 757 | // 根据文件位置和md5值来判断是否重复 758 | const key = parent + '|' + md5Checksum // + '|' + name 759 | if (exists[key]) { 760 | dupe_files.push(file) 761 | } else { 762 | exists[key] = true 763 | } 764 | } 765 | return dupe_files.concat(dupe_empty_folders) 766 | } 767 | 768 | async function confirm_dedupe ({ file_number, folder_number }) { 769 | const answer = await prompts({ 770 | type: 'select', 771 | name: 'value', 772 | message: `检测到同位置下重复文件${file_number}个,重复空目录${folder_number}个,是否删除?`, 773 | choices: [ 774 | { title: 'Yes', description: '确认删除', value: 'yes' }, 775 | { title: 'No', description: '先不删除', value: 'no' } 776 | ], 777 | initial: 0 778 | }) 779 | return answer.value 780 | } 781 | 782 | // 需要sa是源文件夹所在盘的manager 783 | async function mv_file ({ fid, new_parent, service_account }) { 784 | const file = await get_info_by_id(fid, service_account) 785 | if (!file) return 786 | const removeParents = file.parents[0] 787 | let url = `https://www.googleapis.com/drive/v3/files/${fid}` 788 | const params = { 789 | removeParents, 790 | supportsAllDrives: true, 791 | addParents: new_parent 792 | } 793 | url += '?' + params_to_query(params) 794 | const headers = await gen_headers(service_account) 795 | return axins.patch(url, {}, { headers }) 796 | } 797 | 798 | // 将文件或文件夹移入回收站,需要 sa 为 content manager 权限及以上 799 | async function trash_file ({ fid, service_account }) { 800 | const url = `https://www.googleapis.com/drive/v3/files/${fid}?supportsAllDrives=true` 801 | const headers = await gen_headers(service_account) 802 | return axins.patch(url, { trashed: true }, { headers }) 803 | } 804 | 805 | // 直接删除文件或文件夹,不会进入回收站,需要 sa 为 manager 权限 806 | async function rm_file ({ fid, service_account }) { 807 | const headers = await gen_headers(service_account) 808 | let retry = 0 809 | const url = `https://www.googleapis.com/drive/v3/files/${fid}?supportsAllDrives=true` 810 | while (retry < RETRY_LIMIT) { 811 | try { 812 | return await axins.delete(url, { headers }) 813 | } catch (err) { 814 | retry++ 815 | handle_error(err) 816 | console.log('删除重试中,重试次数', retry) 817 | } 818 | } 819 | } 820 | 821 | async function dedupe ({ fid, update, service_account, yes }) { 822 | let arr 823 | if (!update) { 824 | const info = get_all_by_fid(fid) 825 | if (info) { 826 | console.log('找到本地缓存数据,缓存时间:', dayjs(info.mtime).format('YYYY-MM-DD HH:mm:ss')) 827 | arr = info 828 | } 829 | } 830 | arr = arr || await walk_and_save({ fid, update, service_account }) 831 | const dupes = find_dupe(arr) 832 | const folder_number = dupes.filter(v => v.mimeType === FOLDER_TYPE).length 833 | const file_number = dupes.length - folder_number 834 | const choice = yes || await confirm_dedupe({ file_number, folder_number }) 835 | if (choice === 'no') { 836 | return console.log('退出程序') 837 | } else if (!choice) { 838 | return // ctrl+c 839 | } 840 | const limit = pLimit(PARALLEL_LIMIT) 841 | let folder_count = 0 842 | let file_count = 0 843 | await Promise.all(dupes.map(async v => { 844 | try { 845 | await limit(() => trash_file({ fid: v.id, service_account })) 846 | if (v.mimeType === FOLDER_TYPE) { 847 | console.log('成功删除文件夹', v.name) 848 | folder_count++ 849 | } else { 850 | console.log('成功删除文件', v.name) 851 | file_count++ 852 | } 853 | } catch (e) { 854 | console.log('删除失败', v) 855 | handle_error(e) 856 | } 857 | })) 858 | return { file_count, folder_count } 859 | } 860 | 861 | function handle_error (err) { 862 | const data = err && err.response && err.response.data 863 | if (data) { 864 | const message = data.error && data.error.message 865 | if (message && message.toLowerCase().includes('rate limit')) return 866 | console.error(JSON.stringify(data)) 867 | } else { 868 | if (!err.message.includes('timeout')) console.error(err.message) 869 | } 870 | } 871 | 872 | function print_progress (msg) { 873 | if (process.stdout.cursorTo) { 874 | process.stdout.cursorTo(0) 875 | process.stdout.write(msg + ' ') 876 | } else { 877 | console.log(msg) 878 | } 879 | } 880 | 881 | module.exports = { ls_folder, count, validate_fid, copy, dedupe, copy_file, gen_count_body, real_copy, get_name_by_id, get_info_by_id, get_access_token, get_sa_token, walk_and_save } 882 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | const Router = require('@koa/router') 2 | 3 | const { db } = require('../db') 4 | const { validate_fid, gen_count_body } = require('./gd') 5 | const { send_count, send_help, send_choice, send_task_info, sm, extract_fid, extract_from_text, reply_cb_query, tg_copy, send_all_tasks, send_bm_help, get_target_by_alias, send_all_bookmarks, set_bookmark, unset_bookmark, clear_tasks, send_task_help, rm_task } = require('./tg') 6 | 7 | const { AUTH, ROUTER_PASSKEY, TG_IPLIST } = require('../config') 8 | const { tg_whitelist } = AUTH 9 | 10 | const COPYING_FIDS = {} 11 | const counting = {} 12 | const router = new Router() 13 | 14 | router.get('/api/gdurl/count', async ctx => { 15 | if (!ROUTER_PASSKEY) return ctx.body = 'gd-utils 成功启动' 16 | const { query, headers } = ctx.request 17 | let { fid, type, update, passkey } = query 18 | if (passkey !== ROUTER_PASSKEY) return ctx.body = 'invalid passkey' 19 | if (!validate_fid(fid)) throw new Error('无效的分享ID') 20 | 21 | let ua = headers['user-agent'] || '' 22 | ua = ua.toLowerCase() 23 | type = (type || '').toLowerCase() 24 | // todo type=tree 25 | if (!type) { 26 | if (ua.includes('curl')) { 27 | type = 'curl' 28 | } else if (ua.includes('mozilla')) { 29 | type = 'html' 30 | } else { 31 | type = 'json' 32 | } 33 | } 34 | if (type === 'html') { 35 | ctx.set('Content-Type', 'text/html; charset=utf-8') 36 | } else if (['json', 'all'].includes(type)) { 37 | ctx.set('Content-Type', 'application/json; charset=UTF-8') 38 | } 39 | ctx.body = await gen_count_body({ fid, type, update, service_account: true }) 40 | }) 41 | 42 | router.post('/api/gdurl/tgbot', async ctx => { 43 | const { body } = ctx.request 44 | console.log('ctx.ip', ctx.ip) // 可以只允许tg服务器的ip 45 | console.log('tg message:', JSON.stringify(body, null, ' ')) 46 | if (TG_IPLIST && !TG_IPLIST.includes(ctx.ip)) return ctx.body = 'invalid ip' 47 | ctx.body = '' // 早点释放连接 48 | const message = body.message || body.edited_message 49 | const message_str = JSON.stringify(message) 50 | 51 | const { callback_query } = body 52 | if (callback_query) { 53 | const { id, message, data } = callback_query 54 | const chat_id = callback_query.from.id 55 | const [action, fid, target] = data.split(' ').filter(v => v) 56 | if (action === 'count') { 57 | if (counting[fid]) return sm({ chat_id, text: fid + ' 正在统计,请稍等片刻' }) 58 | counting[fid] = true 59 | send_count({ fid, chat_id }).catch(err => { 60 | console.error(err) 61 | sm({ chat_id, text: fid + ' 统计失败:' + err.message }) 62 | }).finally(() => { 63 | delete counting[fid] 64 | }) 65 | } else if (action === 'copy') { 66 | if (COPYING_FIDS[fid]) return sm({ chat_id, text: `正在处理 ${fid} 的复制命令` }) 67 | COPYING_FIDS[fid] = true 68 | tg_copy({ fid, target: get_target_by_alias(target), chat_id }).then(task_id => { 69 | task_id && sm({ chat_id, text: `开始复制,任务ID: ${task_id} 可输入 /task ${task_id} 查询进度` }) 70 | }).finally(() => COPYING_FIDS[fid] = false) 71 | } else if (action === 'update') { 72 | if (counting[fid]) return sm({ chat_id, text: fid + ' 正在统计,请稍等片刻' }) 73 | counting[fid] = true 74 | send_count({ fid, chat_id, update: true }).finally(() => { 75 | delete counting[fid] 76 | }) 77 | } else if (action === 'clear_button') { 78 | const { message_id, text } = message || {} 79 | if (message_id) sm({ chat_id, message_id, text, parse_mode: 'HTML' }, 'editMessageText') 80 | } 81 | return reply_cb_query({ id, data }).catch(console.error) 82 | } 83 | 84 | const chat_id = message && message.chat && message.chat.id 85 | const text = (message && message.text && message.text.trim()) || '' 86 | let username = message && message.from && message.from.username 87 | username = username && String(username).toLowerCase() 88 | let user_id = message && message.from && message.from.id 89 | user_id = user_id && String(user_id).toLowerCase() 90 | if (!chat_id || !tg_whitelist.some(v => { 91 | v = String(v).toLowerCase() 92 | return v === username || v === user_id 93 | })) { 94 | chat_id && sm({ chat_id, text: '您的用户名或ID不在机器人的白名单中,如果是您配置的机器人,请先到config.js中配置自己的username' }) 95 | return console.warn('收到非白名单用户的请求') 96 | } 97 | 98 | const fid = extract_fid(text) || extract_from_text(text) || extract_from_text(message_str) 99 | const no_fid_commands = ['/task', '/help', '/bm'] 100 | if (!no_fid_commands.some(cmd => text.startsWith(cmd)) && !validate_fid(fid)) { 101 | return sm({ chat_id, text: '未识别出分享ID' }) 102 | } 103 | if (text.startsWith('/help')) return send_help(chat_id) 104 | if (text.startsWith('/bm')) { 105 | const [cmd, action, alias, target] = text.split(' ').map(v => v.trim()).filter(v => v) 106 | if (!action) return send_all_bookmarks(chat_id) 107 | if (action === 'set') { 108 | if (!alias || !target) return sm({ chat_id, text: '别名和目标ID不能为空' }) 109 | if (alias.length > 24) return sm({ chat_id, text: '别名不要超过24个英文字符长度' }) 110 | if (!validate_fid(target)) return sm({ chat_id, text: '目标ID格式有误' }) 111 | set_bookmark({ chat_id, alias, target }) 112 | } else if (action === 'unset') { 113 | if (!alias) return sm({ chat_id, text: '别名不能为空' }) 114 | unset_bookmark({ chat_id, alias }) 115 | } else { 116 | send_bm_help(chat_id) 117 | } 118 | } else if (text.startsWith('/count')) { 119 | if (counting[fid]) return sm({ chat_id, text: fid + ' 正在统计,请稍等片刻' }) 120 | try { 121 | counting[fid] = true 122 | const update = text.endsWith(' -u') 123 | await send_count({ fid, chat_id, update }) 124 | } catch (err) { 125 | console.error(err) 126 | sm({ chat_id, text: fid + ' 统计失败:' + err.message }) 127 | } finally { 128 | delete counting[fid] 129 | } 130 | } else if (text.startsWith('/copy')) { 131 | let target = text.replace('/copy', '').replace(' -u', '').trim().split(' ').map(v => v.trim()).filter(v => v)[1] 132 | target = get_target_by_alias(target) || target 133 | if (target && !validate_fid(target)) return sm({ chat_id, text: `目标ID ${target} 格式不正确` }) 134 | const update = text.endsWith(' -u') 135 | tg_copy({ fid, target, chat_id, update }).then(task_id => { 136 | task_id && sm({ chat_id, text: `开始复制,任务ID: ${task_id} 可输入 /task ${task_id} 查询进度` }) 137 | }) 138 | } else if (text.startsWith('/task')) { 139 | let task_id = text.replace('/task', '').trim() 140 | if (task_id === 'all') { 141 | return send_all_tasks(chat_id) 142 | } else if (task_id === 'clear') { 143 | return clear_tasks(chat_id) 144 | } else if (task_id === '-h') { 145 | return send_task_help(chat_id) 146 | } else if (task_id.startsWith('rm')) { 147 | task_id = task_id.replace('rm', '') 148 | task_id = parseInt(task_id) 149 | if (!task_id) return send_task_help(chat_id) 150 | return rm_task({ task_id, chat_id }) 151 | } 152 | task_id = parseInt(task_id) 153 | if (!task_id) { 154 | const running_tasks = db.prepare('select id from task where status=?').all('copying') 155 | if (!running_tasks.length) return sm({ chat_id, text: '当前暂无运行中的任务' }) 156 | return running_tasks.forEach(v => send_task_info({ chat_id, task_id: v.id }).catch(console.error)) 157 | } 158 | send_task_info({ task_id, chat_id }).catch(console.error) 159 | } else if (message_str.includes('drive.google.com/') || validate_fid(text)) { 160 | return send_choice({ fid: fid || text, chat_id }) 161 | } else { 162 | sm({ chat_id, text: '暂不支持此命令' }) 163 | } 164 | }) 165 | 166 | module.exports = router 167 | -------------------------------------------------------------------------------- /src/summary.js: -------------------------------------------------------------------------------- 1 | const Table = require('cli-table3') 2 | const colors = require('colors/safe') 3 | const { escape } = require('html-escaper') 4 | 5 | module.exports = { make_table, summary, make_html, make_tg_table, format_size } 6 | 7 | function make_html ({ file_count, folder_count, total_size, details }) { 8 | const head = ['类型', '数量', '大小'] 9 | const th = '' + head.map(k => `${k}`).join('') + '' 10 | const td = details.map(v => '' + [escape(v.ext), v.count, v.size].map(k => `${k}`).join('') + '').join('') 11 | let tail = ['合计', file_count + folder_count, total_size] 12 | tail = '' + tail.map(k => `${k}`).join('') + '' 13 | const table = ` 14 | ${th} 15 | ${td} 16 | ${tail} 17 |
` 18 | return table 19 | } 20 | 21 | function make_table ({ file_count, folder_count, total_size, details }) { 22 | const tb = new Table() 23 | const hAlign = 'center' 24 | const headers = ['Type', 'Count', 'Size'].map(v => ({ content: colors.bold.brightBlue(v), hAlign })) 25 | const records = details.map(v => [v.ext, v.count, v.size]).map(arr => { 26 | return arr.map(content => ({ content, hAlign })) 27 | }) 28 | const total_count = file_count + folder_count 29 | const tails = ['总计', total_count, total_size].map(v => ({ content: colors.bold(v), hAlign })) 30 | tb.push(headers, ...records) 31 | tb.push(tails) 32 | return tb.toString() + '\n' 33 | } 34 | 35 | function make_tg_table ({ file_count, folder_count, total_size, details }) { 36 | const tb = new Table({ 37 | // chars: { 38 | // 'top': '═', 39 | // 'top-mid': '╤', 40 | // 'top-left': '╔', 41 | // 'top-right': '╗', 42 | // 'bottom': '═', 43 | // 'bottom-mid': '╧', 44 | // 'bottom-left': '╚', 45 | // 'bottom-right': '╝', 46 | // 'left': '║', 47 | // 'left-mid': '╟', 48 | // 'right': '║', 49 | // 'right-mid': '╢' 50 | // }, 51 | style: { 52 | head: [], 53 | border: [] 54 | } 55 | }) 56 | const hAlign = 'center' 57 | const headers = ['Type', 'Count', 'Size'].map(v => ({ content: v, hAlign })) 58 | details.forEach(v => { 59 | if (v.ext === '文件夹') v.ext = '[Folder]' 60 | if (v.ext === '无扩展名') v.ext = '[NoExt]' 61 | }) 62 | const records = details.map(v => [v.ext, v.count, v.size]).map(arr => arr.map(content => ({ content, hAlign }))) 63 | const total_count = file_count + folder_count 64 | const tails = ['Total', total_count, total_size].map(v => ({ content: v, hAlign })) 65 | tb.push(headers, ...records) 66 | tb.push(tails) 67 | return tb.toString().replace(/─/g, '—') // 防止在手机端表格换行 去掉replace后在pc端更美观 68 | } 69 | 70 | function summary (info, sort_by) { 71 | const files = info.filter(v => v.mimeType !== 'application/vnd.google-apps.folder') 72 | const file_count = files.length 73 | const folder_count = info.filter(v => v.mimeType === 'application/vnd.google-apps.folder').length 74 | let total_size = info.map(v => Number(v.size) || 0).reduce((acc, val) => acc + val, 0) 75 | total_size = format_size(total_size) 76 | const exts = {} 77 | const sizes = {} 78 | let no_ext = 0; let no_ext_size = 0 79 | files.forEach(v => { 80 | let { name, size } = v 81 | size = Number(size) || 0 82 | const ext = name.split('.').pop().toLowerCase() 83 | if (!name.includes('.') || ext.length > 10) { // 若 . 后超过10字符,判断为无扩展名 84 | no_ext_size += size 85 | return no_ext++ 86 | } 87 | if (exts[ext]) { 88 | exts[ext]++ 89 | } else { 90 | exts[ext] = 1 91 | } 92 | if (sizes[ext]) { 93 | sizes[ext] += size 94 | } else { 95 | sizes[ext] = size 96 | } 97 | }) 98 | const details = Object.keys(exts).map(ext => { 99 | const count = exts[ext] 100 | const size = sizes[ext] 101 | return { ext, count, size: format_size(size), raw_size: size } 102 | }) 103 | if (sort_by === 'size') { 104 | details.sort((a, b) => b.raw_size - a.raw_size) 105 | } else if (sort_by === 'name') { 106 | details.sort((a, b) => (a.ext > b.ext) ? 1 : -1) 107 | } else { 108 | details.sort((a, b) => b.count - a.count) 109 | } 110 | if (no_ext) details.push({ ext: '无扩展名', count: no_ext, size: format_size(no_ext_size), raw_size: no_ext_size }) 111 | if (folder_count) details.push({ ext: '文件夹', count: folder_count, size: 0, raw_size: 0 }) 112 | return { file_count, folder_count, total_size, details } 113 | } 114 | 115 | function format_size (n) { 116 | n = Number(n) 117 | if (Number.isNaN(n)) return '' 118 | if (n < 0) return 'invalid size' 119 | const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] 120 | let flag = 0 121 | while (n >= 1024) { 122 | n = (n / 1024) 123 | flag++ 124 | } 125 | return n.toFixed(2) + ' ' + units[flag] 126 | } 127 | -------------------------------------------------------------------------------- /src/tg.js: -------------------------------------------------------------------------------- 1 | const Table = require('cli-table3') 2 | const dayjs = require('dayjs') 3 | const axios = require('@viegg/axios') 4 | const HttpsProxyAgent = require('https-proxy-agent') 5 | 6 | const { db } = require('../db') 7 | const { gen_count_body, validate_fid, real_copy, get_name_by_id, get_info_by_id, copy_file } = require('./gd') 8 | const { AUTH, DEFAULT_TARGET, USE_PERSONAL_AUTH } = require('../config') 9 | const { tg_token } = AUTH 10 | const gen_link = (fid, text) => `${text || fid}` 11 | 12 | if (!tg_token) throw new Error('请先在config.js里设置tg_token') 13 | const { https_proxy } = process.env 14 | const axins = axios.create(https_proxy ? { httpsAgent: new HttpsProxyAgent(https_proxy) } : {}) 15 | 16 | const FID_TO_NAME = {} 17 | 18 | async function get_folder_name (fid) { 19 | let name = FID_TO_NAME[fid] 20 | if (name) return name 21 | name = await get_name_by_id(fid, !USE_PERSONAL_AUTH) 22 | return FID_TO_NAME[fid] = name 23 | } 24 | 25 | function send_help (chat_id) { 26 | const text = `
[使用帮助]
 27 | 命令 | 说明
 28 | =====================
 29 | /help | 返回本条使用说明
 30 | =====================
 31 | /count shareID [-u] | 返回sourceID的文件统计信息
 32 | sourceID可以是google drive分享网址本身,也可以是分享ID。如果命令最后加上 -u,则无视之前的记录强制从线上获取,适合一段时候后才更新完毕的分享链接。
 33 | =====================
 34 | /copy sourceID targetID [-u] | 将sourceID的文件复制到targetID里(会新建一个文件夹)
 35 | 若不填targetID,则会复制到默认位置(在config.js里设置)。
 36 | 如果设置了bookmark,那么targetID可以是bookmark的别名。
 37 | 如果命令最后加上 -u,则无视本地缓存强制从线上获取源文件夹信息。
 38 | 命令开始执行后会回复此次任务的taskID。
 39 | =====================
 40 | /task | 返回对应任务的进度信息
 41 | 用例:
 42 | /task | 返回所有正在运行的任务详情
 43 | /task 7 | 返回编号为 7 的任务详情
 44 | /task all | 返回所有任务记录列表
 45 | /task clear | 清除所有状态为已完成的任务记录
 46 | /task rm 7 | 删除编号为 7 的任务记录
 47 | =====================
 48 | /bm [action] [alias] [target] | bookmark,添加常用目的文件夹ID
 49 | 会在输入网址后返回的「文件统计」「开始复制」这两个按钮的下方出现,方便复制到常用位置。
 50 | 用例:
 51 | /bm | 返回所有设置的收藏夹
 52 | /bm set movie folder-id | 将folder-id添加到收藏夹,别名设为movie
 53 | /bm unset movie | 删除此收藏夹
 54 | 
` 55 | return sm({ chat_id, text, parse_mode: 'HTML' }) 56 | } 57 | 58 | function send_bm_help (chat_id) { 59 | const text = `
/bm [action] [alias] [target] | bookmark,添加常用目的文件夹ID
 60 | 会在输入网址后返回的「文件统计」「开始复制」这两个按钮的下方出现,方便复制到常用位置。
 61 | 用例:
 62 | /bm | 返回所有设置的收藏夹
 63 | /bm set movie folder-id | 将folder-id添加到收藏夹,别名设为movie
 64 | /bm unset movie | 删除此收藏夹
 65 | 
` 66 | return sm({ chat_id, text, parse_mode: 'HTML' }) 67 | } 68 | 69 | function send_task_help (chat_id) { 70 | const text = `
/task [action/id] [id] | 查询或管理任务进度
 71 | 用例:
 72 | /task | 返回所有正在运行的任务详情
 73 | /task 7 | 返回编号为 7 的任务详情
 74 | /task all | 返回所有任务记录列表
 75 | /task clear | 清除所有状态为已完成的任务记录
 76 | /task rm 7 | 删除编号为 7 的任务记录
 77 | 
` 78 | return sm({ chat_id, text, parse_mode: 'HTML' }) 79 | } 80 | 81 | function clear_tasks (chat_id) { 82 | const finished_tasks = db.prepare('select id from task where status=?').all('finished') 83 | finished_tasks.forEach(task => rm_task({ task_id: task.id })) 84 | sm({ chat_id, text: '已清除所有状态为已完成的任务记录' }) 85 | } 86 | 87 | function rm_task ({ task_id, chat_id }) { 88 | const exist = db.prepare('select id from task where id=?').get(task_id) 89 | if (!exist) return sm({ chat_id, text: `不存在编号为 ${task_id} 的任务记录` }) 90 | db.prepare('delete from task where id=?').run(task_id) 91 | db.prepare('delete from copied where taskid=?').run(task_id) 92 | if (chat_id) sm({ chat_id, text: `已删除任务 ${task_id} 记录` }) 93 | } 94 | 95 | function send_all_bookmarks (chat_id) { 96 | let records = db.prepare('select alias, target from bookmark').all() 97 | if (!records.length) return sm({ chat_id, text: '数据库中没有收藏记录' }) 98 | const tb = new Table({ style: { head: [], border: [] } }) 99 | const headers = ['别名', '目录ID'] 100 | records = records.map(v => [v.alias, v.target]) 101 | tb.push(headers, ...records) 102 | const text = tb.toString().replace(/─/g, '—') 103 | return sm({ chat_id, text: `
${text}
`, parse_mode: 'HTML' }) 104 | } 105 | 106 | function set_bookmark ({ chat_id, alias, target }) { 107 | const record = db.prepare('select alias from bookmark where alias=?').get(alias) 108 | if (record) return sm({ chat_id, text: '数据库中已有同名的收藏' }) 109 | db.prepare('INSERT INTO bookmark (alias, target) VALUES (?, ?)').run(alias, target) 110 | return sm({ chat_id, text: `成功设置收藏:${alias} | ${target}` }) 111 | } 112 | 113 | function unset_bookmark ({ chat_id, alias }) { 114 | const record = db.prepare('select alias from bookmark where alias=?').get(alias) 115 | if (!record) return sm({ chat_id, text: '未找到此别名的收藏' }) 116 | db.prepare('delete from bookmark where alias=?').run(alias) 117 | return sm({ chat_id, text: '成功删除收藏 ' + alias }) 118 | } 119 | 120 | function get_target_by_alias (alias) { 121 | const record = db.prepare('select target from bookmark where alias=?').get(alias) 122 | return record && record.target 123 | } 124 | 125 | function get_alias_by_target (target) { 126 | const record = db.prepare('select alias from bookmark where target=?').get(target) 127 | return record && record.alias 128 | } 129 | 130 | function send_choice ({ fid, chat_id }) { 131 | return sm({ 132 | chat_id, 133 | text: `识别出分享ID ${fid},请选择动作`, 134 | reply_markup: { 135 | inline_keyboard: [ 136 | [ 137 | { text: '文件统计', callback_data: `count ${fid}` }, 138 | { text: '开始复制', callback_data: `copy ${fid}` } 139 | ], 140 | [ 141 | { text: '强制刷新', callback_data: `update ${fid}` }, 142 | { text: '清除按钮', callback_data: `clear_button` } 143 | ] 144 | ].concat(gen_bookmark_choices(fid)) 145 | } 146 | }) 147 | } 148 | 149 | // console.log(gen_bookmark_choices()) 150 | function gen_bookmark_choices (fid) { 151 | const gen_choice = v => ({ text: `复制到 ${v.alias}`, callback_data: `copy ${fid} ${v.alias}` }) 152 | const records = db.prepare('select * from bookmark').all() 153 | const result = [] 154 | for (let i = 0; i < records.length; i += 2) { 155 | const line = [gen_choice(records[i])] 156 | if (records[i + 1]) line.push(gen_choice(records[i + 1])) 157 | result.push(line) 158 | } 159 | return result 160 | } 161 | 162 | async function send_all_tasks (chat_id) { 163 | let records = db.prepare('select id, status, ctime from task').all() 164 | if (!records.length) return sm({ chat_id, text: '数据库中没有任务记录' }) 165 | const tb = new Table({ style: { head: [], border: [] } }) 166 | const headers = ['ID', 'status', 'ctime'] 167 | records = records.map(v => { 168 | const { id, status, ctime } = v 169 | return [id, status, dayjs(ctime).format('YYYY-MM-DD HH:mm:ss')] 170 | }) 171 | tb.push(headers, ...records) 172 | const text = tb.toString().replace(/─/g, '—') 173 | const url = `https://api.telegram.org/bot${tg_token}/sendMessage` 174 | return axins.post(url, { 175 | chat_id, 176 | parse_mode: 'HTML', 177 | text: `所有拷贝任务:\n
${text}
` 178 | }).catch(err => { 179 | // const description = err.response && err.response.data && err.response.data.description 180 | // if (description && description.includes('message is too long')) { 181 | if (true) { 182 | const text = [headers].concat(records.slice(-100)).map(v => v.join('\t')).join('\n') 183 | return sm({ chat_id, parse_mode: 'HTML', text: `所有拷贝任务(只显示最近100条):\n
${text}
` }) 184 | } 185 | console.error(err) 186 | }) 187 | } 188 | 189 | async function get_task_info (task_id) { 190 | const record = db.prepare('select * from task where id=?').get(task_id) 191 | if (!record) return {} 192 | const { source, target, status, mapping, ctime, ftime } = record 193 | const { copied_files } = db.prepare('select count(fileid) as copied_files from copied where taskid=?').get(task_id) 194 | const folder_mapping = mapping && mapping.trim().split('\n') 195 | const new_folder = folder_mapping && folder_mapping[0].split(' ')[1] 196 | const { summary } = db.prepare('select summary from gd where fid=?').get(source) || {} 197 | const { file_count, folder_count, total_size } = summary ? JSON.parse(summary) : {} 198 | const total_count = (file_count || 0) + (folder_count || 0) 199 | const copied_folders = folder_mapping ? (folder_mapping.length - 1) : 0 200 | let text = '任务编号:' + task_id + '\n' 201 | const folder_name = await get_folder_name(source) 202 | text += '源文件夹:' + gen_link(source, folder_name) + '\n' 203 | text += '目的位置:' + gen_link(target, get_alias_by_target(target)) + '\n' 204 | text += '新文件夹:' + (new_folder ? gen_link(new_folder) : '暂未创建') + '\n' 205 | text += '任务状态:' + status + '\n' 206 | text += '创建时间:' + dayjs(ctime).format('YYYY-MM-DD HH:mm:ss') + '\n' 207 | text += '完成时间:' + (ftime ? dayjs(ftime).format('YYYY-MM-DD HH:mm:ss') : '未完成') + '\n' 208 | text += '目录进度:' + copied_folders + '/' + (folder_count === undefined ? '未知数量' : folder_count) + '\n' 209 | text += '文件进度:' + copied_files + '/' + (file_count === undefined ? '未知数量' : file_count) + '\n' 210 | text += '总百分比:' + ((copied_files + copied_folders) * 100 / total_count).toFixed(2) + '%\n' 211 | text += '合计大小:' + (total_size || '未知大小') 212 | return { text, status, folder_count } 213 | } 214 | 215 | async function send_task_info ({ task_id, chat_id }) { 216 | const { text, status, folder_count } = await get_task_info(task_id) 217 | if (!text) return sm({ chat_id, text: '数据库不存在此任务ID:' + task_id }) 218 | const url = `https://api.telegram.org/bot${tg_token}/sendMessage` 219 | let message_id 220 | try { 221 | const { data } = await axins.post(url, { chat_id, text, parse_mode: 'HTML' }) 222 | message_id = data && data.result && data.result.message_id 223 | } catch (e) { 224 | console.log('fail to send message to tg', e.message) 225 | } 226 | // get_task_info 在task目录数超大时比较吃cpu,以后如果最好把mapping也另存一张表 227 | if (!message_id || status !== 'copying') return 228 | const loop = setInterval(async () => { 229 | const { text, status } = await get_task_info(task_id) 230 | if (status !== 'copying') clearInterval(loop) 231 | sm({ chat_id, message_id, text, parse_mode: 'HTML' }, 'editMessageText') 232 | }, 10 * 1000) 233 | } 234 | 235 | async function tg_copy ({ fid, target, chat_id, update }) { // return task_id 236 | target = target || DEFAULT_TARGET 237 | if (!target) { 238 | sm({ chat_id, text: '请输入目的地ID或先在config.js里设置默认复制目的地ID(DEFAULT_TARGET)' }) 239 | return 240 | } 241 | const file = await get_info_by_id(fid, !USE_PERSONAL_AUTH) 242 | if (file && file.mimeType !== 'application/vnd.google-apps.folder') { 243 | return copy_file(fid, target, !USE_PERSONAL_AUTH).then(data => { 244 | sm({ chat_id, parse_mode: 'HTML', text: `复制单文件成功,文件位置:${gen_link(target)}` }) 245 | }).catch(e => { 246 | sm({ chat_id, text: `复制单文件失败,失败消息:${e.message}` }) 247 | }) 248 | } 249 | 250 | let record = db.prepare('select id, status from task where source=? and target=?').get(fid, target) 251 | if (record) { 252 | if (record.status === 'copying') { 253 | sm({ chat_id, text: '已有相同源ID和目的ID的任务正在进行,查询进度可输入 /task ' + record.id }) 254 | return 255 | } else if (record.status === 'finished') { 256 | sm({ chat_id, text: `检测到已存在的任务 ${record.id},开始继续拷贝` }) 257 | } 258 | } 259 | 260 | real_copy({ source: fid, update, target, service_account: !USE_PERSONAL_AUTH, is_server: true }) 261 | .then(async info => { 262 | if (!record) record = {} // 防止无限循环 263 | if (!info) return 264 | const { task_id } = info 265 | const { text } = await get_task_info(task_id) 266 | sm({ chat_id, text, parse_mode: 'HTML' }) 267 | }) 268 | .catch(err => { 269 | const task_id = record && record.id 270 | if (task_id) db.prepare('update task set status=? where id=?').run('error', task_id) 271 | if (!record) record = {} 272 | console.error('复制失败', fid, '-->', target) 273 | console.error(err) 274 | sm({ chat_id, text: (task_id || '') + '任务出错,错误消息:' + err.message }) 275 | }) 276 | 277 | while (!record) { 278 | record = db.prepare('select id from task where source=? and target=?').get(fid, target) 279 | await sleep(1000) 280 | } 281 | return record.id 282 | } 283 | 284 | function sleep (ms) { 285 | return new Promise((resolve, reject) => { 286 | setTimeout(resolve, ms) 287 | }) 288 | } 289 | 290 | function reply_cb_query ({ id, data }) { 291 | const url = `https://api.telegram.org/bot${tg_token}/answerCallbackQuery` 292 | return axins.post(url, { 293 | callback_query_id: id, 294 | text: '开始执行 ' + data 295 | }) 296 | } 297 | 298 | async function send_count ({ fid, chat_id, update }) { 299 | sm({ chat_id, text: `开始获取 ${fid} 所有文件信息,请稍后,建议统计完成前先不要开始复制,因为复制也需要先获取源文件夹信息` }) 300 | const table = await gen_count_body({ fid, update, type: 'tg', service_account: !USE_PERSONAL_AUTH }) 301 | if (!table) return sm({ chat_id, parse_mode: 'HTML', text: gen_link(fid) + ' 信息获取失败' }) 302 | const url = `https://api.telegram.org/bot${tg_token}/sendMessage` 303 | const gd_link = `https://drive.google.com/drive/folders/${fid}` 304 | const name = await get_folder_name(fid) 305 | return axins.post(url, { 306 | chat_id, 307 | parse_mode: 'HTML', 308 | text: `
源文件夹名称:${name}
309 | 源链接:${gd_link}
310 | ${table}
` 311 | }).catch(async err => { 312 | // const description = err.response && err.response.data && err.response.data.description 313 | // const too_long_msgs = ['request entity too large', 'message is too long'] 314 | // if (description && too_long_msgs.some(v => description.toLowerCase().includes(v))) { 315 | if (true) { 316 | const smy = await gen_count_body({ fid, type: 'json', service_account: !USE_PERSONAL_AUTH }) 317 | const { file_count, folder_count, total_size } = JSON.parse(smy) 318 | return sm({ 319 | chat_id, 320 | parse_mode: 'HTML', 321 | text: `链接:${fid}\n
322 | 表格太长超出telegram消息限制,只显示概要:
323 | 目录名称:${name}
324 | 文件总数:${file_count}
325 | 目录总数:${folder_count}
326 | 合计大小:${total_size}
327 | 
` 328 | }) 329 | } 330 | throw err 331 | }) 332 | } 333 | 334 | function sm (data, endpoint) { 335 | endpoint = endpoint || 'sendMessage' 336 | const url = `https://api.telegram.org/bot${tg_token}/${endpoint}` 337 | return axins.post(url, data).catch(err => { 338 | // console.error('fail to post', url, data) 339 | console.error('fail to send message to tg:', err.message) 340 | const err_data = err.response && err.response.data 341 | err_data && console.error(err_data) 342 | }) 343 | } 344 | 345 | function extract_fid (text) { 346 | text = text.replace(/^\/count/, '').replace(/^\/copy/, '').replace(/\\/g, '').trim() 347 | const [source, target] = text.split(' ').map(v => v.trim()) 348 | if (validate_fid(source)) return source 349 | try { 350 | if (!text.startsWith('http')) text = 'https://' + text 351 | const u = new URL(text) 352 | if (u.pathname.includes('/folders/')) { 353 | return u.pathname.split('/').map(v => v.trim()).filter(v => v).pop() 354 | } else if (u.pathname.includes('/file/')) { 355 | const file_reg = /file\/d\/([a-zA-Z0-9_-]+)/ 356 | const file_match = u.pathname.match(file_reg) 357 | return file_match && file_match[1] 358 | } 359 | return u.searchParams.get('id') 360 | } catch (e) { 361 | return '' 362 | } 363 | } 364 | 365 | function extract_from_text (text) { 366 | // const reg = /https?:\/\/drive.google.com\/[^\s]+/g 367 | const reg = /https?:\/\/drive.google.com\/[a-zA-Z0-9_\\/?=&-]+/g 368 | const m = text.match(reg) 369 | return m && extract_fid(m[0]) 370 | } 371 | 372 | module.exports = { send_count, send_help, sm, extract_fid, reply_cb_query, send_choice, send_task_info, send_all_tasks, tg_copy, extract_from_text, get_target_by_alias, send_bm_help, send_all_bookmarks, set_bookmark, unset_bookmark, clear_tasks, send_task_help, rm_task } 373 | -------------------------------------------------------------------------------- /src/tree.js: -------------------------------------------------------------------------------- 1 | module.exports = { gen_tree_html } 2 | 3 | function gen_tree_html (arr) { 4 | const data = gen_tree_data(arr, is_gd_folder) 5 | return tree_tpl(JSON.stringify(data)) 6 | } 7 | 8 | function tree_tpl (str) { 9 | return ` 10 | 11 | 12 | 13 | 14 | 15 | 16 | Folder Tree 17 | 18 | 19 | 20 | 21 | 22 |
23 | 26 | 27 | 28 | 29 | ` 30 | } 31 | 32 | function is_gd_folder (data) { 33 | return data.mimeType === 'application/vnd.google-apps.folder' 34 | } 35 | 36 | function gen_tree_data (data, is_folder) { 37 | if (!data || !data.length) return [] 38 | const folders = data.filter(is_folder) 39 | const files = data.filter(v => !is_folder(v)) 40 | const total_size = sum(files.map(v => v.size)) 41 | const root = { 42 | title: `/根目录 [共${files.length} 个文件(不包括文件夹), ${format_size(total_size)}]`, 43 | key: data[0].parent 44 | } 45 | if (!folders.length) return [root] 46 | const sub_folders = folders.filter(v => v.parent === folders[0].parent) 47 | sub_folders.forEach(v => { 48 | sum_files(v, data, is_folder) 49 | count_files(v, data, is_folder) 50 | }) 51 | sort_folders(folders, 'count') 52 | sort_folders(sub_folders, 'count') 53 | folders.forEach(v => { 54 | let {name, size, count, id} = v 55 | if (name.length > 50) name = name.slice(0, 48) + '...' 56 | v.title = `${name} | [共${count}个文件 ${format_size(size)}]` 57 | }) 58 | root.children = sub_folders.map(v => gen_node(v, folders)) 59 | return [root] 60 | } 61 | 62 | function sort_folders (folders, type) { 63 | if (!folders || !folders.length) return 64 | if (type === 'size') return folders.sort((a, b) => b.size - a.size) 65 | if (type === 'count') return folders.sort((a, b) => b.count - a.count) 66 | } 67 | 68 | function gen_node (v, folders) { 69 | const {id, title, node} = v 70 | if (node) return node 71 | return v.node = { 72 | title, 73 | key: id, 74 | children: v.children || folders.filter(vv => vv.parent === id).map(vv => gen_node(vv, folders)) 75 | } 76 | } 77 | 78 | function count_files (folder, arr, is_folder) { 79 | if (folder.count) return folder.count 80 | const children = arr.filter(v => v.parent === folder.id) 81 | return folder.count = sum(children.map(v => { 82 | if (is_folder(v)) return count_files(v, arr, is_folder) 83 | return 1 84 | })) 85 | } 86 | 87 | function sum_files (folder, arr, is_folder) { 88 | if (folder.size) return folder.size 89 | const children = arr.filter(v => v.parent === folder.id) 90 | return folder.size = sum(children.map(v => { 91 | if (is_folder(v)) return sum_files(v, arr, is_folder) 92 | return v.size 93 | })) 94 | } 95 | 96 | function sum (arr) { 97 | let result = 0 98 | for (const v of arr) { 99 | result += Number(v) || 0 100 | } 101 | return result 102 | } 103 | 104 | function format_size (n) { 105 | n = Number(n) 106 | if (Number.isNaN(n)) return '' 107 | if (n < 0) return 'invalid size' 108 | const units = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] 109 | let flag = 0 110 | while (n >= 1024) { 111 | n = n / 1024 112 | flag++ 113 | } 114 | return n.toFixed(2) + ' ' + units[flag] 115 | } 116 | -------------------------------------------------------------------------------- /static/autorclone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErics/gd-utils/f4c8450a3f81ba65c7208031d9fdfbb2eeb7980c/static/autorclone.png -------------------------------------------------------------------------------- /static/choose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErics/gd-utils/f4c8450a3f81ba65c7208031d9fdfbb2eeb7980c/static/choose.png -------------------------------------------------------------------------------- /static/colab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErics/gd-utils/f4c8450a3f81ba65c7208031d9fdfbb2eeb7980c/static/colab.png -------------------------------------------------------------------------------- /static/count.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErics/gd-utils/f4c8450a3f81ba65c7208031d9fdfbb2eeb7980c/static/count.png -------------------------------------------------------------------------------- /static/error-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErics/gd-utils/f4c8450a3f81ba65c7208031d9fdfbb2eeb7980c/static/error-log.png -------------------------------------------------------------------------------- /static/gclone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErics/gd-utils/f4c8450a3f81ba65c7208031d9fdfbb2eeb7980c/static/gclone.png -------------------------------------------------------------------------------- /static/gdurl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErics/gd-utils/f4c8450a3f81ba65c7208031d9fdfbb2eeb7980c/static/gdurl.png -------------------------------------------------------------------------------- /static/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iErics/gd-utils/f4c8450a3f81ba65c7208031d9fdfbb2eeb7980c/static/tree.png -------------------------------------------------------------------------------- /validate-sa.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { argv } = require('yargs') 4 | .usage('用法: ./$0 folder-id\nfolder-id 是你想检测SA是否对其有阅读权限的目录ID') 5 | .help('h') 6 | .alias('h', 'help') 7 | 8 | const fs = require('fs') 9 | const path = require('path') 10 | const prompts = require('prompts') 11 | const { GoogleToken } = require('gtoken') 12 | const axios = require('@viegg/axios') 13 | const HttpsProxyAgent = require('https-proxy-agent') 14 | 15 | const { https_proxy } = process.env 16 | const axins = axios.create(https_proxy ? { httpsAgent: new HttpsProxyAgent(https_proxy) } : {}) 17 | 18 | const SA_FILES = fs.readdirSync(path.join(__dirname, 'sa')).filter(v => v.endsWith('.json')) 19 | const SA_TOKENS = SA_FILES.map(filename => { 20 | const gtoken = new GoogleToken({ 21 | keyFile: path.join(__dirname, 'sa', filename), 22 | scope: ['https://www.googleapis.com/auth/drive'] 23 | }) 24 | return {gtoken, filename} 25 | }) 26 | 27 | main() 28 | async function main () { 29 | const [fid] = argv._ 30 | if (validate_fid(fid)) { 31 | console.log('开始检测', SA_TOKENS.length, '个SA帐号') 32 | const invalid_sa = await get_invalid_sa(SA_TOKENS, fid) 33 | if (!invalid_sa.length) return console.log('已检测', SA_TOKENS.length, '个SA,未检测到无效帐号') 34 | const choice = await choose(invalid_sa.length) 35 | if (choice === 'yes') { 36 | mv_sa(invalid_sa) 37 | console.log('成功移动') 38 | } else { 39 | console.log('成功退出,无效的SA记录:', invalid_sa) 40 | } 41 | } else { 42 | console.warn('目录ID缺失或格式错误') 43 | } 44 | } 45 | 46 | function mv_sa (arr) { 47 | for (const filename of arr) { 48 | const oldpath = path.join(__dirname, 'sa', filename) 49 | const new_path = path.join(__dirname, 'sa/invalid', filename) 50 | fs.renameSync(oldpath, new_path) 51 | } 52 | } 53 | 54 | 55 | async function choose (count) { 56 | const answer = await prompts({ 57 | type: 'select', 58 | name: 'value', 59 | message: `检测到 ${count} 个无效的SA,是否将它们移动到 sa/invalid 目录下?`, 60 | choices: [ 61 | { title: 'Yes', description: '确认移动', value: 'yes' }, 62 | { title: 'No', description: '不做更改,直接退出', value: 'no' } 63 | ], 64 | initial: 0 65 | }) 66 | return answer.value 67 | } 68 | 69 | /*async function user_choose () { 70 | const response = await prompts({ 71 | type: 'number', 72 | name: 'value', 73 | message: `检测到 ${count} 个无效的SA,是否将它们移动到 sa/invalid 目录下?\n 请输入选择(1/2)`, 74 | message: `请输入选择(1/2/3): 75 | 1、Yes 76 | 2、No 77 | `, 78 | validate: value => [1, 2].includes(value) ? true : `必须输入 1/2` 79 | }) 80 | const choices = ['', 'continue', 'restart'] 81 | return choices[response.value] 82 | }*/ 83 | 84 | async function get_invalid_sa (arr, fid) { 85 | if (!fid) throw new Error('请指定要检测权限的目录ID') 86 | const fails = [] 87 | let flag = 0 88 | let good = 0 89 | for (const v of arr) { 90 | console.log('检测进度', `${flag++}/${arr.length}`) 91 | console.log('正常/异常', `${good}/${fails.length}`) 92 | const {gtoken, filename} = v 93 | try { 94 | const access_token = await get_sa_token(gtoken) 95 | await get_info(fid, access_token) 96 | good++ 97 | } catch (e) { 98 | handle_error(e) 99 | const status = e && e.response && e.response.status 100 | if (Number(status) === 400) fails.push(filename) // access_token 获取失败 101 | 102 | const data = e && e.response && e.response.data 103 | const code = data && data.error && data.error.code 104 | if ([404, 403].includes(Number(code))) fails.push(filename) // 读取文件夹信息失败 105 | } 106 | } 107 | return fails 108 | } 109 | 110 | function handle_error (err) { 111 | const data = err && err.response && err.response.data 112 | if (data) { 113 | console.error(JSON.stringify(data)) 114 | } else { 115 | console.error(err.message) 116 | } 117 | } 118 | 119 | async function get_info (fid, access_token) { 120 | let url = `https://www.googleapis.com/drive/v3/files/${fid}` 121 | let params = { 122 | includeItemsFromAllDrives: true, 123 | supportsAllDrives: true, 124 | corpora: 'allDrives', 125 | fields: 'id,name' 126 | } 127 | url += '?' + params_to_query(params) 128 | const headers = { authorization: 'Bearer ' + access_token } 129 | const { data } = await axins.get(url, { headers }) 130 | return data 131 | } 132 | 133 | function params_to_query (data) { 134 | const ret = [] 135 | for (let d in data) { 136 | ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d])) 137 | } 138 | return ret.join('&') 139 | } 140 | 141 | async function get_sa_token (gtoken) { 142 | return new Promise((resolve, reject) => { 143 | gtoken.getToken((err, tk) => { 144 | err ? reject(err) : resolve(tk.access_token) 145 | }) 146 | }) 147 | } 148 | 149 | function validate_fid (fid) { 150 | if (!fid) return false 151 | fid = String(fid) 152 | if (fid.length < 10 || fid.length > 100) return false 153 | const reg = /^[a-zA-Z0-9_-]+$/ 154 | return fid.match(reg) 155 | } 156 | --------------------------------------------------------------------------------