├── .gitignore ├── 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 ├── gdutils.bat ├── gdutils.sh ├── package-lock.json ├── package.json ├── readme.md ├── sa.sh ├── sa ├── .keep └── invalid │ └── .keep ├── server.js ├── src ├── gd.js ├── router.js ├── snap2html.js ├── summary.js ├── tg.js └── tree.js ├── static ├── autorclone.png ├── choose.png ├── colab.png ├── count.png ├── error-log.png ├── gclone.png ├── gdurl.png ├── snap2html-example.html ├── snap2html.template ├── 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 | -------------------------------------------------------------------------------- /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('Usage: ./$0 [options]') 14 | .alias('o', 'output') 15 | .describe('output', 'Specify Output File,Do not fill in the default url.txt') 16 | .alias('u', 'update') 17 | .describe('u', 'Do not use local cache, force to obtain source folder information online') 18 | .alias('S', 'service_account') 19 | .describe('S', 'Use service account to operate, provided that the sa authorized json file must be placed in the ./sa directory') 20 | .alias('k', 'hashkey') 21 | .describe('k', 'Use the hashkey set by the website deployed at https://github.com/iwestlin/gdshare to generate a legal download link') 22 | .alias('c', 'cf') 23 | .describe('cf', 'Website URL deployed using gdshare') 24 | .alias('e', 'expire') 25 | .describe('e', 'gdshare direct link expiration time, unit hour, default value 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('Generated', output) 36 | console.log('Execute the command to download:\n', cmd) 37 | }) 38 | .catch(console.error) 39 | } else { 40 | console.warn('FolderID is wrong or invalid') 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 `# File Size:${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/roshanconnor123/gd-utils/f09caa7557ce862c5017f8700af0c30c589b3e7c/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-07-10] 5 | - 添加树形导出类型,示例用法: `./count folder-id -S -t tree -o tree.html` 6 | 7 | `tree.html`可直接用浏览器打开: 8 | ![](./static/tree.png) 9 | 10 | 前端源码:[https://github.com/iwestlin/foldertree/blob/master/app.jsx](https://github.com/iwestlin/foldertree/blob/master/app.jsx) 11 | 12 | [2020-07-08] 13 | - 添加[colab脚本](https://github.com/iwestlin/gd-utils/issues/50#issuecomment-655298073) 14 | 15 | [2020-07-07] 16 | - 在复制文件时不使用p-limit依赖,改为while循环控制并行请求数,从而大大减少复制十万及以上数量级文件时的内存占用,避免进程被node强行退出。 17 | - 给机器人添加更多 /task 功能,支持清除所有已完成任务、删除特定任务 18 | 19 | [2020-07-06] 20 | - 给机器人添加收藏功能,[使用示例](https://drive.google.com/drive/folders/1sW8blrDT8o7882VOpXXr3pzXR73d4yGX) 21 | 22 | [2020-07-05] 23 | - pm2 启动脚本换成 `pm2 start server.js --node-args="--max-old-space-size=4096"`,避免任务文件数超大时内存占用太高被node干掉。 24 | 25 | [2020-07-04]**【重要更新】** 26 | - 解决了长时间拷贝命令突然出现 `Invalid Credentials` 错误的问题。 27 | 原因是依赖的[gtoken](https://www.npmjs.com/package/gtoken)在过期时间后并不返回新的access_token...之前有不少朋友遇到过,一开始我还以为是sa同时使用太多触发了Google限制,直到我自己将sa分批使用量降到了50却也依然遇到了这种报错…… 28 | - 提升了拷贝大量文件时数据库的操作效率,大大减少了cpu占用。(由于更改了数据库的结构,所以如果有未完成的任务,请先跑完任务再更新。如果更新代码后再继续之前未完成的任务,会导致无法接上进度。) 29 | - 如果触发团队盘40万文件数限制,会返回明确的错误消息,而不是之前的 `创建目录失败,请检查您的账号是否有相关权限` 30 | - 如果创建目录未完成被中断,相同命令重新开始执行后,会保留原始目录的结构继续创建目录。(之前会导致结构被打乱) 31 | 32 | [2020-07-03] 33 | - 给命令行 ./copy 命令添加了 `-D` 选项,表示不在目的地创建同名文件夹,直接将源文件夹中的文件原样复制到目的文件夹中 34 | 35 | [2020-07-02] 36 | - 机器人 /task 命令返回的进度信息每 10 秒更新一次 37 | - `./dedupe` 改为将重复文件移动到回收站(需要内容管理者及以上权限) 38 | - 给 sqlite 打开 WAL 模式提升效率 39 | - 提前5分钟将access_token判定为过期,减少未授权错误 40 | 41 | [2020-07-01](建议所有使用tg机器人的用户更新) 42 | - 给机器人的 `/count` 和 `/copy` 命令添加了 `-u` 参数的支持,命令最后加上 -u 表示强制从线上获取源文件夹信息 43 | - 允许继续状态为已完成的任务(适合搭配 -u 参数,增量复制刚分享出来的未更新完毕的文件夹) 44 | - 支持识别转发的 [@gdurl](https://t.me/s/gdurl) 频道消息中的google drive链接 45 | - 机器人回复任务完成消息时,携带 `成功拷贝目录(文件)数/总目录(文件)数` 46 | - 机器人回复统计消息时,携带文件夹名称 47 | - 机器人回复`/task`消息时,携带源文件夹名称链接和新文件夹链接 48 | - 当统计表格太长导致机器人发送消息失败时,发送统计概要 49 | - 增加了 [专家设置](#专家设置) 一节,保障HTTPS接口安全 50 | 51 | [2020-06-30] 52 | - 命令行操作时,不换行输出进度信息,同时将进度信息输出间隔调整为1秒 53 | - 隐藏 timeout exceed 报错信息 54 | 55 | ## 重要更新(2020-06-29) 56 | 如果你遇到了以下几种问题,请务必阅读此节: 57 | 58 | - 任务异常中断 59 | - 命令行日志无限循环输出但进度不变 60 | - 复制完发现丢文件 61 | 62 | 有不少网友遇到这些问题,但是作者一直无法复现,直到有tg网友发了张运行日志截图: 63 | ![](./static/error-log.png) 64 | 报错日志的意思是找不到对应的目录ID,这种情况会发生在SA没有对应目录的阅读权限的时候。 65 | 当进行server side copy时,需要向Google的服务器提交要复制的文件ID,和复制的位置,也就是新创建的目录ID,由于在请求时是随机选取的SA,所以当选中没有权限的SA时,这次拷贝请求没有对应目录的权限,就会发生图中的错误。 66 | 67 | **所以,上述这些问题的源头是,sa目录下,混杂了没有权限的json文件!** 68 | 69 | 以下是解决办法: 70 | - 在项目目录下,执行 `git pull` 拉取最新代码 71 | - 执行 `./validate-sa.js -h` 查看使用说明 72 | - 选择一个你的sa拥有阅读权限的目录ID,执行 `./validate-sa.js 你的目录ID` 73 | 74 | 程序会读取sa目录下所有json文件,依次检查它们是否拥有对 `你的目录ID` 的阅读权限,如果最后发现了无效的SA,程序会提供选项允许用户将无效的sa json移动到特定目录。 75 | 76 | 将无效sa文件移动以后,如果你使用了pm2启动,需要 `pm2 reload server` 重启下进程。 77 | 78 | 操作示例: [https://drive.google.com/drive/folders/1iiTAzWF_v9fo_IxrrMYiRGQ7QuPrnxHf](https://drive.google.com/drive/folders/1iiTAzWF_v9fo_IxrrMYiRGQ7QuPrnxHf) 79 | -------------------------------------------------------------------------------- /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('Deleted', record.c, 'Data') 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 | // How many milliseconds for a single request to time out(Reference value,If continuous timeout, it will be adjusted to twice the previous time) 2 | const TIMEOUT_BASE = 7000 3 | // Maximum timeout setting,For example, for a certain request, the first 7s timeout, the second 14s, the third 28s, the fourth 56s, the fifth is not 112s but 60 4 | const TIMEOUT_MAX = 60000 5 | 6 | const LOG_DELAY = 5000 // Log output interval, in milliseconds 7 | const PAGE_SIZE = 1000 // Each network request to read the number of files in the directory, the larger the value, the more likely it will time out, and it should not exceed 1000 8 | 9 | const RETRY_LIMIT = 7 // The maximum number of retries allowed, If a request fails 10 | const PARALLEL_LIMIT = 20 // The number of parallel network requests can be adjusted according to the network environment 11 | 12 | const DEFAULT_TARGET = '' // Required. Copy the default destination ID. If target is not specified, it will be copied here. It is recommended to fill in the team drive ID 13 | 14 | const AUTH = { // If you have the json authorization file of the service account, you can copy it to the sa directory instead of 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, // Can be left blank 19 | access_token: '', // Can be left blank 20 | tg_token: 'bot_token', // Your telegram bot token,Go here https://core.telegram.org/bots#6-botfather 21 | tg_whitelist: ['your_tg_username'] // Your tg username(t.me/username),The bot will only execute commands sent by users in this list 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('Usage: ./$0 [options]\ndestination folderid is Optional,If its not filled, it use DEFAULT_TARGET in config.js') 7 | .alias('u', 'update') 8 | .describe('u', 'Do not use local cache, force to obtain source folder information online') 9 | .alias('y', 'yes') 10 | .describe('yes', 'If a copy record is found, resume without asking') 11 | .alias('f', 'file') 12 | .describe('f', 'Copy a single file') 13 | .alias('n', 'name') 14 | .describe('n', 'Rename the target folder, leave the original folder name blank') 15 | .alias('N', 'not_teamdrive') 16 | .describe('N', 'If it is not a team drive link, you can add this parameter to improve interface query efficiency and reduce latency') 17 | .alias('s', 'size') 18 | .describe('s', 'If you add this, all files are copied by default. If this value is set, files smaller than this size will be filtered out - must end with b, such as 10mb') 19 | .alias('S', 'service_account') 20 | .describe('S', 'Specify the service account for operation, provided that the json authorization file must be placed in the /sa Folder, please ensure that the SA account has Proper permissions。') 21 | .alias('D', 'dncnr') 22 | .describe('D', 'do not create new root, Does not create a folder with the same name at the destination, will directly copy the files in the source folder to the destination folder as they are') 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 Incorrect Format') 36 | return copy_file(source, target, service_account).then(r => { 37 | const link = 'https://drive.google.com/drive/folders/' + target 38 | console.log('Task is completed,File Location:\n', link) 39 | }).catch(console.error) 40 | } 41 | let min_size 42 | if (size) { 43 | console.log(`Do not Copy Size File Below ${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('\nTask Completed,Folder Link:\n', link) 50 | }) 51 | } else { 52 | console.warn('FolderID is wrong or invalid') 53 | } 54 | -------------------------------------------------------------------------------- /count: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { argv } = require('yargs') 4 | .usage('Usage: ./$0 [options]') 5 | .example('./$0 1ULY8ISgWSOVc0UrzejykVgXfVL_I4r75', 'Get statistics of all files contained in https://drive.google.com/drive/folders/1ULY8ISgWSOVc0UrzejykVgXfVL_I4r75') 6 | .example('./$0 root -s size -t html -o out.html', 'Get the personal drive root directory statistics, the results are output in HTML form, sorted in reverse order according to the total size, and saved to the out.html file in this directory (create new if it does not exist, overwrite if it exists) ') 7 | .example('./$0 root -s name -t json -o out.json', 'Get the statistics information of the root directory of the personal drive. The results are output in JSON format, sorted by file extension, and saved to the out.json file in this directory') 8 | .example('./$0 root -t all -o all.json', 'Get the statistics of the root Folder of the personal drive, output all file information (including folders) in JSON format, and save it to the all.json file in this Folder') 9 | .alias('u', 'update') 10 | .describe('u', 'Force to get information online (regardless of whether there is a local cache)') 11 | .alias('N', 'not_teamdrive') 12 | .describe('N', 'If it is not a team drive link, you can add this parameter to improve interface query efficiency and reduce latency. If you want to count a personal drive and the service account in the ./sa directory does not have relevant permissions, please make sure to add this flag to use personal auth information for query') 13 | .alias('S', 'service_account') 14 | .describe('S', 'Specify the use of service account for statistics,The thing is that the SA json file must be placed in the sa Folder') 15 | .alias('s', 'sort') 16 | .describe('s', 'Sorting method of statistical results,Optional value name or size,If it is not filled in, it will be arranged in reverse order according to the number of files by default') 17 | .alias('t', 'type') 18 | .describe('t', 'The output type of the statistical result, the optional value is html/tree/snap/json/all, all means output the data as a json, it is best to use with -o. If not filled, the command line form will be output by default') 19 | .alias('o', 'output') 20 | .describe('o', 'Statistics output file, suitable to use with -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('FolderID is wrong or invalid') 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('Usage: ./$0 [options]') 5 | .alias('y', 'yes') 6 | .describe('yes', 'If duplicate items are found, delete them without asking') 7 | .alias('u', 'update') 8 | .describe('u', 'Do not use local cache, force to obtain source folder information online') 9 | .alias('S', 'service_account') 10 | .describe('S', 'Use service account to operate, provided that the sa authorized json file must be placed in the ./sa directory') 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('The task is completed, the total number of deleted files:', file_count, 'Number of Folders:', folder_count) 23 | }) 24 | } else { 25 | console.warn('FolderID is wrong or invalid') 26 | } 27 | -------------------------------------------------------------------------------- /doc/bot-worked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roshanconnor123/gd-utils/f09caa7557ce862c5017f8700af0c30c589b3e7c/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/roshanconnor123/gd-utils/f09caa7557ce862c5017f8700af0c30c589b3e7c/gdurl.sqlite -------------------------------------------------------------------------------- /gdutils.bat: -------------------------------------------------------------------------------- 1 | @ECHO off 2 | title Gdutils is bae 3 | color 0b 4 | ECHO. 5 | ECHO Gdutils by iwestlin! - English version by Roshanconnor! 6 | ECHO ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 7 | ECHO. 8 | ECHO What would you like to do ? 9 | ECHO ---------------------------------------------------------------------------------------------------------------------- 10 | ECHO off 11 | 12 | :option 13 | ECHO. 14 | ECHO 1) COPY 15 | ECHO 2) SIZE 16 | ECHO 3) DEDUPE 17 | ECHO 4) EXIT 18 | ECHO. 19 | SET /P option="Choose your Option: " 20 | if %option% == 1 (goto copy) 21 | if %option% == 2 (goto size) 22 | if %option% == 3 (goto dedupe) 23 | if %option% == 4 (EXIT) 24 | ECHO. 25 | 26 | :copy 27 | ECHO. 28 | ECHO 1) Create a New Folder with the same name (as in source) in the destination as well 29 | ECHO 2) Donot create a new Folder in the destination 30 | ECHO. 31 | SET /P cp="Choose your Mode " 32 | ECHO ---------------------------------------------------------------------------------------------------------------------- 33 | SET /P SRC="[Enter Source Folder ID] " 34 | SET /P DST="[Enter Destination Folder ID] " 35 | ECHO. 36 | if %cp% == 1 (node --max-old-space-size=512 copy %SRC% %DST% -S) 37 | if %cp% == 2 (node --max-old-space-size=512 copy %SRC% %DST% -S -D) 38 | ECHO. 39 | pause 40 | goto option 41 | 42 | 43 | :size 44 | ECHO. 45 | ECHO 1) Normal Size Info 46 | ECHO 2) Create a html file with tree like pattern 47 | ECHO 3) Create a snap2html index 48 | ECHO. 49 | SET /P sz="Choose your Mode " 50 | ECHO ---------------------------------------------------------------------------------------------------------------------- 51 | SET /P SRC="[Enter Folder ID] " 52 | ECHO. 53 | if %sz% == 1 (node count %SRC% -S) 54 | if %sz% == 2 (node count %SRC% -S -t tree -o tree.html) 55 | if %sz% == 3 (node count %SRC% -S -t snap -o index.html) 56 | ECHO Check your Gd-utils folder to Find the html File 57 | ECHO. 58 | pause 59 | goto option 60 | 61 | 62 | :dedupe 63 | ECHO. 64 | SET /P SRC="[Enter Folder ID] " 65 | ECHO. 66 | node dedupe %SRC% -S 67 | ECHO. 68 | pause 69 | goto option 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /gdutils.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #============================================================= 3 | # https://github.com/roshanconnor123/gd-utils 4 | # File Name: gdutils.sh 5 | # Author: roshanconnor 6 | # Description:running gdutils 7 | # System Required: Debian/Ubuntu 8 | #============================================================= 9 | 10 | cecho() { 11 | local code="\033[" 12 | case "$1" in 13 | black | bk) color="${code}0;30m";; 14 | red | r) color="${code}1;31m";; 15 | green | g) color="${code}1;32m";; 16 | yellow | y) color="${code}1;33m";; 17 | blue | b) color="${code}1;34m";; 18 | purple | p) color="${code}1;35m";; 19 | cyan | c) color="${code}1;36m";; 20 | gray | gr) color="${code}0;37m";; 21 | *) local text="$1" 22 | esac 23 | [ -z "$text" ] && local text="$color$2${code}0m" 24 | echo -e "$text" 25 | } 26 | 27 | # ★★★Copy from source to destination★★★ 28 | copy() { 29 | cd ~ && cd gd-utils 30 | cecho r "Remember to add your SAs as Viewer in source TD and as a Contributor in Destination TD" 31 | echo "Provide Source Folder ID" 32 | read SRC 33 | echo "Provide Destination Folder ID" 34 | read DST 35 | cecho r "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" 36 | echo && echo "Copy mode selected" 37 | 38 | cecho p "A.Create a New Folder with the same name in the destination" 39 | cecho p "B.Do not create a new Folder in the destination" && echo 40 | read -p " Choose A/B:" option 41 | 42 | case "$option" in 43 | A) 44 | node --max-old-space-size=512 copy $SRC $DST -S 45 | ;; 46 | B) 47 | node --max-old-space-size=512 copy $SRC $DST -S -D 48 | ;; 49 | *) 50 | echo 51 | cecho r "Choose the Correct Option" 52 | ;; 53 | esac 54 | } 55 | # ★★★Calculate the size★★★ 56 | count() { 57 | cd ~ && cd gd-utils 58 | cecho r "Remember to add your SAs as Viewer (atleast) in source TD" 59 | echo "Provide Folder ID" 60 | read SRC 61 | cecho r "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" 62 | echo && echo "Size mode selected" 63 | 64 | cecho p "A.Normal Size Info" 65 | cecho p "B.Create a html file with tree like pattern" 66 | cecho p "C.Create a snap2html index" && echo 67 | read -p "Choose A/B/C:" option 68 | 69 | case "$option" in 70 | A) 71 | node count $SRC -S 72 | ;; 73 | B) 74 | node count $SRC -S -t tree -o /sdcard/Tree.html 75 | echo " Check your Internal storage to find a file called Tree.html" 76 | ;; 77 | C) 78 | node count $SRC -S -t snap -o /sdcard/Index.html 79 | echo "Check your Internal Storag to find a file called Index.html" 80 | ;; 81 | *) 82 | echo 83 | cecho r "Choose the Correct Option" 84 | ;; 85 | esac 86 | } 87 | # ★★★Dedupe The Folder★★★ 88 | dedupe() { 89 | cd ~ && cd gd-utils 90 | cecho r "Remember to add your SAs as Content manager (atleast) in source TD\n" 91 | echo "Provide Folder ID\n" 92 | read SRC 93 | node dedupe $SRC -S 94 | } 95 | 96 | 97 | # ★★★Running Gdutils★★★ 98 | printf "%s by %s -English version by %s\n" "$(cecho b GD-UTILS)" "$(cecho r iwestlin)" "$(cecho c Roshanconnor)" 99 | 100 | cecho g "1.Copy Files to your Teamdrive" 101 | echo "➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖" 102 | cecho g "2.Calculate Size" 103 | echo "➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖" 104 | cecho g "3.Remove Duplicate Files" 105 | echo "➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖" 106 | cecho y "4.EXIT" && echo 107 | read -p " Choose any Number [1-4]:" option 108 | 109 | case "$option" in 110 | 1) 111 | copy 112 | ;; 113 | 2) 114 | count 115 | ;; 116 | 3) 117 | dedupe 118 | ;; 119 | 4) 120 | exit 121 | ;; 122 | *) 123 | echo 124 | cecho r "Choose Correct Number from the Options" 125 | ;; 126 | esac 127 | -------------------------------------------------------------------------------- /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 | # Gd-Utils 2 | 3 | Gd-Utils is just another tool which helps in bypassing the 750GB daily transfer limit by Google. 4 | 5 | :octocat: This fork is an English version of Gd-Utils by [@iwestlin](https://github.com/iwestlin) 6 | 7 | https://github.com/iwestlin/gd-utils 8 | 9 | :octocat: All I have done is to edit the code and use Google Translate to translate Chinese to English. So all credits to the OP. 10 | 11 | :octocat: I have only included the installation part for running `Gd-Utils` on your system ([Telegram Bot part is here](https://github.com/roshanconnor123/Gdutils_Tgbot)) 12 | ## General Instructions 13 | Like other tools (Autorclone/Folderclone/Gclone/Fclone) Gd-Utils is also based upon Service Accounts aka SAs. 14 | 15 | Among these tools only `Autorclone` and `Folderclone` can generate SAs by themselves. 16 | 17 | >Therefore for this tool to work you need SAs generated using either [Autorclone](https://github.com/xyou365/AutoRclone) or [Folderclone](https://github.com/Spazzlo/folderclone) 18 | 19 | ### 📦 Pre-Requisites: 20 | 21 | This tool can be used on **Microsoft Windows**, **Android** and **GNU/Linux**. 22 | 23 | 📣 You will need already generated **SAs**. 24 | 25 | 📣 If you are using this on **GNU/Linux** or **Android**:- 26 | 27 | Create a new repository on Github and name it as `accounts` then upload all your SAs (.json files) there. 28 | 29 | [Follow this guide to understand better](https://telegra.ph/Uploading-Service-Accounts-to-Github-07-09) 30 | 31 | ## Installation 32 | 33 | ### 🔳 Windows 34 | 35 | 36 | 🌠 Install Node.js on Windows 37 | 38 | Install [Node.js](https://nodejs.org/dist/v12.18.4/node-v12.18.4-x64.msi) and make sure to install additional components (tick mark the option saying `Install Additional Components`). 39 | 40 | It is essential to install the addtional components of Node.js for this tool to work. 41 | 42 | 🌠 Create a new folder onto `Desktop` and name it as `Gd-utils`. 43 | 44 | 🌠 [Download this](https://github.com/roshanconnor123/gd-utils/archive/master.zip) and extract it - copy all the contents to newly created `Gd-utils Folder` in your Desktop. 45 | 46 | 🌠 Open cmd inside `Gd-utils` folder and enter this command 47 | ``` 48 | npm install --unsafe-perm=true --allow-root 49 | ``` 50 | If it shows `0 vulnerabilities`, it means that the installation was successful. 51 | 52 | 🌠 Go to Autorclone/Folderclone folder on your PC and open `Accounts` Folder - Copy all the .json files. 53 | 54 | 🌠 Go to `sa` Folder inside `Gd-utils` folder and paste all the .json files there 55 | 56 | 57 | ### 🔳 Android 58 | 59 | 60 | 🌠 Install [Termux](https://play.google.com/store/apps/details?id=com.termux&hl=en_IN%20%20) - Remember to enable Storage Permission by going to Settings. 61 | 62 | 🌠 Install Node.js, Python & Git with Termux 63 | ``` 64 | pkg install python && pkg install git && pkg install nodejs 65 | ``` 66 | 🌠 Install Gd-utils 67 | ``` 68 | git clone https://github.com/roshanconnor123/gd-utils && cd gd-utils && npm install --unsafe-perm=true --allow-root 69 | ``` 70 | 🌠 Download Service Accounts from your Github repository and configue them for Gdutils 71 | ``` 72 | bash sa.sh 73 | ``` 74 | 75 | ### 🔳 GNU/Linux 76 | 77 | 🌠 Installing the dependancies and Gdutils 78 | ```bash 79 | sudo apt-get install build-essential && sudo apt-get update && curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - && sudo apt-get install -y nodejs && git clone https://github.com/roshanconnor123/gd-utils 80 | ``` 81 | ```bash 82 | cd gd-utils && npm install --unsafe-perm=true --allow-root 83 | ``` 84 | 🌠 Download the Service Accounts from your Github repository and configure them for Gdutils 85 | ```bash 86 | bash sa.sh 87 | ``` 88 | 89 | ## Usage 90 | 🔷 Windows 91 | 92 | Double click on **gdutils.bat** file (In Gd-utils folder) 93 | 94 | 🔷 Android 95 | 96 | Run the following command in **Termux** 97 | ```bash 98 | cd gd-utils && bash gdutils.sh 99 | ``` 100 | 🔷 GNU/Linux 101 | 102 | Run the following command in **Terminal** 103 | ```bash 104 | cd gd-utils && bash gdutils.sh 105 | ``` 106 | ## Contributing 107 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 108 | 109 | 110 | ## License 111 | This project is licenced under the [MIT](https://choosealicense.com/licenses/mit/) licence. 112 | -------------------------------------------------------------------------------- /sa.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #============================================================= 3 | # https://github.com/roshanconnor123/gd-utils 4 | # File Name: sa.sh 5 | # Author: roshanconnor 6 | # Description:Downloading service accounts 7 | # System Required: Debian/Ubuntu 8 | #============================================================= 9 | 10 | cecho() { 11 | local code="\033[" 12 | case "$1" in 13 | black | bk) color="${code}0;30m";; 14 | red | r) color="${code}1;31m";; 15 | green | g) color="${code}1;32m";; 16 | yellow | y) color="${code}1;33m";; 17 | blue | b) color="${code}1;34m";; 18 | purple | p) color="${code}1;35m";; 19 | cyan | c) color="${code}1;36m";; 20 | gray | gr) color="${code}0;37m";; 21 | *) local text="$1" 22 | esac 23 | [ -z "$text" ] && local text="$color$2${code}0m" 24 | echo -e "$text" 25 | } 26 | 27 | # ★★★Downloading Service accounts★★★ 28 | echo && cecho r "Downloading the service accounts from your private repo" 29 | echo "Provide github username" 30 | read username 31 | echo "Provide github password" 32 | read Password 33 | cd ~ 34 | git clone https://"$username":"$Password"@github.com/"$username"/accounts 35 | cp accounts/*.json gd-utils/sa/ 36 | cecho b "Service accounts are added to Gdutils" 37 | exit 38 | 39 | -------------------------------------------------------------------------------- /sa/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roshanconnor123/gd-utils/f09caa7557ce862c5017f8700af0c30c589b3e7c/sa/.keep -------------------------------------------------------------------------------- /sa/invalid/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roshanconnor123/gd-utils/f09caa7557ce862c5017f8700af0c30c589b3e7c/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 | const { snap2html } = require('./snap2html') 20 | 21 | const FILE_EXCEED_MSG = 'The number of files on your team drive has exceeded the limit (400,000), Please move the folder that has not been copied to another team drive, and then run the copy command to resume the transfer' 22 | const FOLDER_TYPE = 'application/vnd.google-apps.folder' 23 | const sleep = ms => new Promise((resolve, reject) => setTimeout(resolve, ms)) 24 | 25 | const { https_proxy, http_proxy, all_proxy } = process.env 26 | const proxy_url = https_proxy || http_proxy || all_proxy 27 | 28 | let axins 29 | if (proxy_url) { 30 | console.log('Use Proxy:', proxy_url) 31 | let ProxyAgent 32 | try { 33 | ProxyAgent = require('proxy-agent') 34 | } catch (e) { // run npm i proxy-agent 35 | ProxyAgent = require('https-proxy-agent') 36 | } 37 | axins = axios.create({ httpsAgent: new ProxyAgent(proxy_url) }) 38 | } else { 39 | axins = axios.create({}) 40 | } 41 | 42 | const SA_LOCATION = argv.sa || 'sa' 43 | const SA_BATCH_SIZE = 1000 44 | const SA_FILES = fs.readdirSync(path.join(__dirname, '..', SA_LOCATION)).filter(v => v.endsWith('.json')) 45 | SA_FILES.flag = 0 46 | let SA_TOKENS = get_sa_batch() 47 | 48 | if (is_pm2()) { 49 | setInterval(() => { 50 | SA_FILES.flag = 0 51 | SA_TOKENS = get_sa_batch() 52 | }, 1000 * 3600 * 2) 53 | } 54 | 55 | // https://github.com/Leelow/is-pm2/blob/master/index.js 56 | function is_pm2 () { 57 | return 'PM2_HOME' in process.env || 'PM2_JSON_PROCESSING' in process.env || 'PM2_CLI' in process.env 58 | } 59 | 60 | function get_sa_batch () { 61 | const new_flag = SA_FILES.flag + SA_BATCH_SIZE 62 | const files = SA_FILES.slice(SA_FILES.flag, new_flag) 63 | SA_FILES.flag = new_flag 64 | return files.map(filename => { 65 | const gtoken = new GoogleToken({ 66 | keyFile: path.join(__dirname, '..', SA_LOCATION, filename), 67 | scope: ['https://www.googleapis.com/auth/drive'] 68 | }) 69 | return { gtoken, expires: 0 } 70 | }) 71 | } 72 | 73 | handle_exit(() => { 74 | // console.log('handle_exit running') 75 | const records = db.prepare('select id from task where status=?').all('copying') 76 | records.forEach(v => { 77 | db.prepare('update task set status=? where id=?').run('interrupt', v.id) 78 | }) 79 | records.length && console.log(records.length, 'task interrupted') 80 | db.close() 81 | }) 82 | 83 | async function gen_count_body ({ fid, type, update, service_account, limit, tg }) { 84 | async function update_info () { 85 | const info = await walk_and_save({ fid, update, service_account, tg }) 86 | return [info, summary(info)] 87 | } 88 | 89 | function render_smy (smy, type, unfinished_number) { 90 | if (!smy) return 91 | if (['html', 'curl', 'tg'].includes(type)) { 92 | smy = (typeof smy === 'object') ? smy : JSON.parse(smy) 93 | const type_func = { 94 | html: make_html, 95 | curl: make_table, 96 | tg: make_tg_table 97 | } 98 | let result = type_func[type](smy, limit) 99 | if (unfinished_number) result += `\nNumber of Folders not read:${unfinished_number}` 100 | return result 101 | } else { // Default output json 102 | return (typeof smy === 'string') ? smy : JSON.stringify(smy) 103 | } 104 | } 105 | const file = await get_info_by_id(fid, service_account) 106 | if (file && file.mimeType !== FOLDER_TYPE) return render_smy(summary([file]), type) 107 | 108 | let info, smy 109 | const record = db.prepare('SELECT * FROM gd WHERE fid = ?').get(fid) 110 | if (!file && !record) { 111 | throw new Error(`Unable to access the link, please check if the link is valid and SA has the appropriate permissions:https://drive.google.com/drive/folders/${fid}`) 112 | } 113 | if (!record || update) { 114 | [info, smy] = await update_info() 115 | } 116 | if (type === 'all') { 117 | info = info || get_all_by_fid(fid) 118 | if (!info) { // Explain that the last statistical process was interrupted 119 | [info] = await update_info() 120 | } 121 | return info && JSON.stringify(info) 122 | } 123 | if (smy) return render_smy(smy, type) 124 | if (record && record.summary) return render_smy(record.summary, type) 125 | info = info || get_all_by_fid(fid) 126 | if (info) { 127 | smy = summary(info) 128 | } else { 129 | [info, smy] = await update_info() 130 | } 131 | return render_smy(smy, type, info.unfinished_number) 132 | } 133 | 134 | async function count ({ fid, update, sort, type, output, not_teamdrive, service_account }) { 135 | sort = (sort || '').toLowerCase() 136 | type = (type || '').toLowerCase() 137 | output = (output || '').toLowerCase() 138 | let out_str 139 | if (!update) { 140 | if (!type && !sort && !output) { 141 | const record = db.prepare('SELECT * FROM gd WHERE fid = ?').get(fid) 142 | const smy = record && record.summary && JSON.parse(record.summary) 143 | if (smy) return console.log(make_table(smy)) 144 | } 145 | const info = get_all_by_fid(fid) 146 | if (info) { 147 | console.log('cached data found in local database, cache time:', dayjs(info.mtime).format('YYYY-MM-DD HH:mm:ss')) 148 | if (type === 'snap') { 149 | const name = await get_name_by_id(fid, service_account) 150 | out_str = snap2html({ root: { name, id: fid }, data: info }) 151 | } else { 152 | out_str = get_out_str({ info, type, sort }) 153 | } 154 | if (output) return fs.writeFileSync(output, out_str) 155 | return console.log(out_str) 156 | } 157 | } 158 | const with_modifiedTime = type === 'snap' 159 | const result = await walk_and_save({ fid, not_teamdrive, update, service_account, with_modifiedTime }) 160 | if (type === 'snap') { 161 | const name = await get_name_by_id(fid, service_account) 162 | out_str = snap2html({ root: { name, id: fid }, data: result }) 163 | } else { 164 | out_str = get_out_str({ info: result, type, sort }) 165 | } 166 | if (output) { 167 | fs.writeFileSync(output, out_str) 168 | } else { 169 | console.log(out_str) 170 | } 171 | } 172 | 173 | function get_out_str ({ info, type, sort }) { 174 | const smy = summary(info, sort) 175 | let out_str 176 | if (type === 'tree') { 177 | out_str = gen_tree_html(info) 178 | } else if (type === 'html') { 179 | out_str = make_html(smy) 180 | } else if (type === 'json') { 181 | out_str = JSON.stringify(smy) 182 | } else if (type === 'all') { 183 | out_str = JSON.stringify(info) 184 | } else { 185 | out_str = make_table(smy) 186 | } 187 | return out_str 188 | } 189 | 190 | function get_all_by_fid (fid) { 191 | const record = db.prepare('SELECT * FROM gd WHERE fid = ?').get(fid) 192 | if (!record) return null 193 | const { info, subf } = record 194 | let result = JSON.parse(info) 195 | result = result.map(v => { 196 | v.parent = fid 197 | return v 198 | }) 199 | if (!subf) return result 200 | return recur(result, JSON.parse(subf)) 201 | 202 | function recur (result, subf) { 203 | if (!subf.length) return result 204 | const arr = subf.map(v => { 205 | const row = db.prepare('SELECT * FROM gd WHERE fid = ?').get(v) 206 | if (!row) return null // If the corresponding fid record is not found, it means that the process was interrupted last time or the folder was not read completely 207 | let info = JSON.parse(row.info) 208 | info = info.map(vv => { 209 | vv.parent = v 210 | return vv 211 | }) 212 | return { info, subf: JSON.parse(row.subf) } 213 | }) 214 | if (arr.some(v => v === null)) return null 215 | const sub_subf = [].concat(...arr.map(v => v.subf).filter(v => v)) 216 | result = result.concat(...arr.map(v => v.info)) 217 | return recur(result, sub_subf) 218 | } 219 | } 220 | 221 | async function walk_and_save ({ fid, not_teamdrive, update, service_account, with_modifiedTime, tg }) { 222 | let result = [] 223 | const unfinished_folders = [] 224 | const limit = pLimit(PARALLEL_LIMIT) 225 | 226 | if (update) { 227 | const exists = db.prepare('SELECT fid FROM gd WHERE fid = ?').get(fid) 228 | exists && db.prepare('UPDATE gd SET summary=? WHERE fid=?').run(null, fid) 229 | } 230 | 231 | const loop = setInterval(() => { 232 | const now = dayjs().format('HH:mm:ss') 233 | const message = `${now} | Copied ${result.length} | Ongoing ${limit.activeCount} | Pending ${limit.pendingCount}` 234 | print_progress(message) 235 | }, 1000) 236 | 237 | const tg_loop = tg && setInterval(() => { 238 | tg({ 239 | obj_count: result.length, 240 | processing_count: limit.activeCount, 241 | pending_count: limit.pendingCount 242 | }) 243 | }, 10 * 1000) 244 | 245 | async function recur (parent) { 246 | let files, should_save 247 | if (update) { 248 | files = await limit(() => ls_folder({ fid: parent, not_teamdrive, service_account, with_modifiedTime })) 249 | should_save = true 250 | } else { 251 | const record = db.prepare('SELECT * FROM gd WHERE fid = ?').get(parent) 252 | if (record) { 253 | files = JSON.parse(record.info) 254 | } else { 255 | files = await limit(() => ls_folder({ fid: parent, not_teamdrive, service_account, with_modifiedTime })) 256 | should_save = true 257 | } 258 | } 259 | if (!files) return 260 | if (files.unfinished) unfinished_folders.push(parent) 261 | should_save && save_files_to_db(parent, files) 262 | const folders = files.filter(v => v.mimeType === FOLDER_TYPE) 263 | files.forEach(v => v.parent = parent) 264 | result = result.concat(files) 265 | return Promise.all(folders.map(v => recur(v.id))) 266 | } 267 | try { 268 | await recur(fid) 269 | } catch (e) { 270 | console.error(e) 271 | } 272 | console.log('\nInfo obtained') 273 | unfinished_folders.length ? console.log('Unread FolderID:', JSON.stringify(unfinished_folders)) : console.log('All Folders have been read') 274 | clearInterval(loop) 275 | if (tg_loop) { 276 | clearInterval(tg_loop) 277 | tg({ 278 | obj_count: result.length, 279 | processing_count: limit.activeCount, 280 | pending_count: limit.pendingCount 281 | }) 282 | } 283 | const smy = unfinished_folders.length ? null : summary(result) 284 | smy && db.prepare('UPDATE gd SET summary=?, mtime=? WHERE fid=?').run(JSON.stringify(smy), Date.now(), fid) 285 | result.unfinished_number = unfinished_folders.length 286 | return result 287 | } 288 | 289 | function save_files_to_db (fid, files) { 290 | // Do not save the folder where the request is not completed, then the next call to get_all_by_id will return null, so call walk_and_save again to try to complete the request for this folder 291 | if (files.unfinished) return 292 | let subf = files.filter(v => v.mimeType === FOLDER_TYPE).map(v => v.id) 293 | subf = subf.length ? JSON.stringify(subf) : null 294 | const exists = db.prepare('SELECT fid FROM gd WHERE fid = ?').get(fid) 295 | if (exists) { 296 | db.prepare('UPDATE gd SET info=?, subf=?, mtime=? WHERE fid=?') 297 | .run(JSON.stringify(files), subf, Date.now(), fid) 298 | } else { 299 | db.prepare('INSERT INTO gd (fid, info, subf, ctime) VALUES (?, ?, ?, ?)') 300 | .run(fid, JSON.stringify(files), subf, Date.now()) 301 | } 302 | } 303 | 304 | async function ls_folder ({ fid, not_teamdrive, service_account, with_modifiedTime }) { 305 | let files = [] 306 | let pageToken 307 | const search_all = { includeItemsFromAllDrives: true, supportsAllDrives: true } 308 | const params = ((fid === 'root') || not_teamdrive) ? {} : search_all 309 | params.q = `'${fid}' in parents and trashed = false` 310 | params.orderBy = 'folder,name desc' 311 | params.fields = 'nextPageToken, files(id, name, mimeType, size, md5Checksum)' 312 | if (with_modifiedTime) { 313 | params.fields = 'nextPageToken, files(id, name, mimeType, modifiedTime, size, md5Checksum)' 314 | } 315 | params.pageSize = Math.min(PAGE_SIZE, 1000) 316 | // const use_sa = (fid !== 'root') && (service_account || !not_teamdrive) // Without parameters, use sa by default 317 | const use_sa = (fid !== 'root') && service_account 318 | // const headers = await gen_headers(use_sa) 319 | // For Folders with a large number of subfolders(1ctMwpIaBg8S1lrZDxdynLXJpMsm5guAl),The access_token may have expired before listing 320 | // Because nextPageToken is needed to get the data of the next page,So you cannot use parallel requests,The test found that each request to obtain 1000 files usually takes more than 20 seconds to complete 321 | const gtoken = use_sa && (await get_sa_token()).gtoken 322 | do { 323 | if (pageToken) params.pageToken = pageToken 324 | let url = 'https://www.googleapis.com/drive/v3/files' 325 | url += '?' + params_to_query(params) 326 | let retry = 0 327 | let data 328 | const payload = { timeout: TIMEOUT_BASE } 329 | while (!data && (retry < RETRY_LIMIT)) { 330 | const access_token = gtoken ? (await gtoken.getToken()).access_token : (await get_access_token()) 331 | const headers = { authorization: 'Bearer ' + access_token } 332 | payload.headers = headers 333 | try { 334 | data = (await axins.get(url, payload)).data 335 | } catch (err) { 336 | handle_error(err) 337 | retry++ 338 | payload.timeout = Math.min(payload.timeout * 2, TIMEOUT_MAX) 339 | } 340 | } 341 | if (!data) { 342 | console.error('Folder is not read completely, Parameters:', params) 343 | files.unfinished = true 344 | return files 345 | } 346 | files = files.concat(data.files) 347 | argv.sfl && console.log('files.length:', files.length) 348 | pageToken = data.nextPageToken 349 | } while (pageToken) 350 | 351 | return files 352 | } 353 | 354 | async function gen_headers (use_sa) { 355 | // use_sa = use_sa && SA_TOKENS.length 356 | const access_token = use_sa ? (await get_sa_token()).access_token : (await get_access_token()) 357 | return { authorization: 'Bearer ' + access_token } 358 | } 359 | 360 | function params_to_query (data) { 361 | const ret = [] 362 | for (let d in data) { 363 | ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d])) 364 | } 365 | return ret.join('&') 366 | } 367 | 368 | async function get_access_token () { 369 | const { expires, access_token, client_id, client_secret, refresh_token } = AUTH 370 | if (expires > Date.now()) return access_token 371 | 372 | const url = 'https://www.googleapis.com/oauth2/v4/token' 373 | const headers = { 'Content-Type': 'application/x-www-form-urlencoded' } 374 | const config = { headers } 375 | const params = { client_id, client_secret, refresh_token, grant_type: 'refresh_token' } 376 | const { data } = await axins.post(url, params_to_query(params), config) 377 | // console.log('Got new token:', data) 378 | AUTH.access_token = data.access_token 379 | AUTH.expires = Date.now() + 1000 * data.expires_in 380 | return data.access_token 381 | } 382 | 383 | // get_sa_token().then(console.log).catch(console.error) 384 | async function get_sa_token () { 385 | if (!SA_TOKENS.length) SA_TOKENS = get_sa_batch() 386 | while (SA_TOKENS.length) { 387 | const tk = get_random_element(SA_TOKENS) 388 | try { 389 | return await real_get_sa_token(tk) 390 | } catch (e) { 391 | console.warn('SA failed to get access_token:', e.message) 392 | SA_TOKENS = SA_TOKENS.filter(v => v.gtoken !== tk.gtoken) 393 | if (!SA_TOKENS.length) SA_TOKENS = get_sa_batch() 394 | } 395 | } 396 | throw new Error('No SA available') 397 | } 398 | 399 | async function real_get_sa_token (el) { 400 | const { value, expires, gtoken } = el 401 | // The reason for passing out gtoken is that when an account is exhausted, it can be filtered accordingly 402 | if (Date.now() < expires) return { access_token: value, gtoken } 403 | const { access_token, expires_in } = await gtoken.getToken({ forceRefresh: true }) 404 | el.value = access_token 405 | el.expires = Date.now() + 1000 * (expires_in - 60 * 5) // 5 mins passed is taken as Expired 406 | return { access_token, gtoken } 407 | } 408 | 409 | function get_random_element (arr) { 410 | return arr[~~(arr.length * Math.random())] 411 | } 412 | 413 | function validate_fid (fid) { 414 | if (!fid) return false 415 | fid = String(fid) 416 | const whitelist = ['root', 'appDataFolder', 'photos'] 417 | if (whitelist.includes(fid)) return true 418 | if (fid.length < 10 || fid.length > 100) return false 419 | const reg = /^[a-zA-Z0-9_-]+$/ 420 | return fid.match(reg) 421 | } 422 | 423 | async function create_folder (name, parent, use_sa, limit) { 424 | let url = `https://www.googleapis.com/drive/v3/files` 425 | const params = { supportsAllDrives: true } 426 | url += '?' + params_to_query(params) 427 | const post_data = { 428 | name, 429 | mimeType: FOLDER_TYPE, 430 | parents: [parent] 431 | } 432 | let retry = 0 433 | let err_message 434 | while (retry < RETRY_LIMIT) { 435 | try { 436 | const headers = await gen_headers(use_sa) 437 | return (await axins.post(url, post_data, { headers })).data 438 | } catch (err) { 439 | err_message = err.message 440 | retry++ 441 | handle_error(err) 442 | const data = err && err.response && err.response.data 443 | const message = data && data.error && data.error.message 444 | if (message && message.toLowerCase().includes('file limit')) { 445 | if (limit) limit.clearQueue() 446 | throw new Error(FILE_EXCEED_MSG) 447 | } 448 | console.log('Creating Folder and Retrying:', name, 'No of retries:', retry) 449 | } 450 | } 451 | throw new Error(err_message + ' Folder Name:' + name) 452 | } 453 | 454 | async function get_name_by_id (fid, use_sa) { 455 | const info = await get_info_by_id(fid, use_sa) 456 | return info ? info.name : fid 457 | } 458 | 459 | async function get_info_by_id (fid, use_sa) { 460 | let url = `https://www.googleapis.com/drive/v3/files/${fid}` 461 | let params = { 462 | includeItemsFromAllDrives: true, 463 | supportsAllDrives: true, 464 | corpora: 'allDrives', 465 | fields: 'id, name, size, parents, mimeType, modifiedTime' 466 | } 467 | url += '?' + params_to_query(params) 468 | let retry = 0 469 | while (retry < RETRY_LIMIT) { 470 | try { 471 | const headers = await gen_headers(use_sa) 472 | const { data } = await axins.get(url, { headers }) 473 | return data 474 | } catch (e) { 475 | retry++ 476 | handle_error(e) 477 | } 478 | } 479 | // throw new Error('Unable to access this FolderID:' + fid) 480 | } 481 | 482 | async function user_choose () { 483 | const answer = await prompts({ 484 | type: 'select', 485 | name: 'value', 486 | message: 'Do you wish to resume?', 487 | choices: [ 488 | { title: 'Continue', description: 'Resume the transfer', value: 'continue' }, 489 | { title: 'Restart', description: 'Restart the process', value: 'restart' }, 490 | { title: 'Exit', description: 'Exit', value: 'exit' } 491 | ], 492 | initial: 0 493 | }) 494 | return answer.value 495 | } 496 | 497 | async function copy ({ source, target, name, min_size, update, not_teamdrive, service_account, dncnr, is_server }) { 498 | target = target || DEFAULT_TARGET 499 | if (!target) throw new Error('Destination ID cannot be empty') 500 | 501 | const file = await get_info_by_id(source, service_account) 502 | if (!file) return console.error(`Unable to access the link, please check if the link is valid and SA has the appropriate permissions:https://drive.google.com/drive/folders/${source}`) 503 | if (file && file.mimeType !== FOLDER_TYPE) { 504 | return copy_file(source, target, service_account).catch(console.error) 505 | } 506 | 507 | const record = db.prepare('select id, status from task where source=? and target=?').get(source, target) 508 | if (record && record.status === 'copying') return console.log('This Task is already running. Force Quit') 509 | 510 | try { 511 | return await real_copy({ source, target, name, min_size, update, dncnr, not_teamdrive, service_account, is_server }) 512 | } catch (err) { 513 | console.error('Error copying folder', err) 514 | const record = db.prepare('select id, status from task where source=? and target=?').get(source, target) 515 | if (record) db.prepare('update task set status=? where id=?').run('error', record.id) 516 | } 517 | } 518 | 519 | // To be resolved: If the user manually interrupts the process with ctrl+c, the request that has been issued will not be recorded in the local database even if it is completed, so duplicate files (folders) may be generated 520 | async function real_copy ({ source, target, name, min_size, update, dncnr, not_teamdrive, service_account, is_server }) { 521 | async function get_new_root () { 522 | if (dncnr) return { id: target } 523 | if (name) { 524 | return create_folder(name, target, service_account) 525 | } else { 526 | const file = await get_info_by_id(source, service_account) 527 | if (!file) throw new Error(`Unable to access the link, please check if the link is valid and SA has the appropriate permissions:https://drive.google.com/drive/folders/${source}`) 528 | return create_folder(file.name, target, service_account) 529 | } 530 | } 531 | 532 | const record = db.prepare('select * from task where source=? and target=?').get(source, target) 533 | if (record) { 534 | const copied = db.prepare('select fileid from copied where taskid=?').all(record.id).map(v => v.fileid) 535 | const choice = (is_server || argv.yes) ? 'continue' : await user_choose() 536 | if (choice === 'exit') { 537 | return console.log('exit the program') 538 | } else if (choice === 'continue') { 539 | let { mapping } = record 540 | const old_mapping = {} 541 | const copied_ids = {} 542 | copied.forEach(id => copied_ids[id] = true) 543 | mapping = mapping.trim().split('\n').map(line => line.split(' ')) 544 | const root = mapping[0][1] 545 | mapping.forEach(arr => old_mapping[arr[0]] = arr[1]) 546 | db.prepare('update task set status=? where id=?').run('copying', record.id) 547 | const arr = await walk_and_save({ fid: source, update, not_teamdrive, service_account }) 548 | let files = arr.filter(v => v.mimeType !== FOLDER_TYPE).filter(v => !copied_ids[v.id]) 549 | if (min_size) files = files.filter(v => v.size >= min_size) 550 | const folders = arr.filter(v => v.mimeType === FOLDER_TYPE) 551 | const all_mapping = await create_folders({ 552 | old_mapping, 553 | source, 554 | folders, 555 | service_account, 556 | root, 557 | task_id: record.id 558 | }) 559 | await copy_files({ files, service_account, root, mapping: all_mapping, task_id: record.id }) 560 | db.prepare('update task set status=?, ftime=? where id=?').run('finished', Date.now(), record.id) 561 | return { id: root, task_id: record.id } 562 | } else if (choice === 'restart') { 563 | const new_root = await get_new_root() 564 | const root_mapping = source + ' ' + new_root.id + '\n' 565 | db.prepare('update task set status=?, mapping=? where id=?').run('copying', root_mapping, record.id) 566 | db.prepare('delete from copied where taskid=?').run(record.id) 567 | // const arr = await walk_and_save({ fid: source, update: true, not_teamdrive, service_account }) 568 | const arr = await walk_and_save({ fid: source, update, not_teamdrive, service_account }) 569 | 570 | let files = arr.filter(v => v.mimeType !== FOLDER_TYPE) 571 | if (min_size) files = files.filter(v => v.size >= min_size) 572 | const folders = arr.filter(v => v.mimeType === FOLDER_TYPE) 573 | console.log('Number of folders to be copied:', folders.length) 574 | console.log('Number of files to be copied:', files.length) 575 | const mapping = await create_folders({ 576 | source, 577 | folders, 578 | service_account, 579 | root: new_root.id, 580 | task_id: record.id 581 | }) 582 | await copy_files({ files, mapping, service_account, root: new_root.id, task_id: record.id }) 583 | db.prepare('update task set status=?, ftime=? where id=?').run('finished', Date.now(), record.id) 584 | return { id: new_root.id, task_id: record.id } 585 | } else { 586 | // ctrl+c Exit 587 | return console.log('Exit') 588 | } 589 | } else { 590 | const new_root = await get_new_root() 591 | const root_mapping = source + ' ' + new_root.id + '\n' 592 | const { lastInsertRowid } = db.prepare('insert into task (source, target, status, mapping, ctime) values (?, ?, ?, ?, ?)').run(source, target, 'copying', root_mapping, Date.now()) 593 | const arr = await walk_and_save({ fid: source, update, not_teamdrive, service_account }) 594 | let files = arr.filter(v => v.mimeType !== FOLDER_TYPE) 595 | if (min_size) files = files.filter(v => v.size >= min_size) 596 | const folders = arr.filter(v => v.mimeType === FOLDER_TYPE) 597 | console.log('Number of folders to be copied:', folders.length) 598 | console.log('Number of files to be copied:', files.length) 599 | const mapping = await create_folders({ 600 | source, 601 | folders, 602 | service_account, 603 | root: new_root.id, 604 | task_id: lastInsertRowid 605 | }) 606 | await copy_files({ files, mapping, service_account, root: new_root.id, task_id: lastInsertRowid }) 607 | db.prepare('update task set status=?, ftime=? where id=?').run('finished', Date.now(), lastInsertRowid) 608 | return { id: new_root.id, task_id: lastInsertRowid } 609 | } 610 | } 611 | 612 | async function copy_files ({ files, mapping, service_account, root, task_id }) { 613 | if (!files.length) return 614 | console.log('\nStarted copying files, total:', files.length) 615 | 616 | const loop = setInterval(() => { 617 | const now = dayjs().format('HH:mm:ss') 618 | const message = `${now} | Number of files copied ${count} | ongoing ${concurrency} | Number of Files Pending ${files.length}` 619 | print_progress(message) 620 | }, 1000) 621 | 622 | let count = 0 623 | let concurrency = 0 624 | let err 625 | do { 626 | if (err) { 627 | clearInterval(loop) 628 | files = null 629 | throw err 630 | } 631 | if (concurrency >= PARALLEL_LIMIT) { 632 | await sleep(100) 633 | continue 634 | } 635 | const file = files.shift() 636 | if (!file) { 637 | await sleep(1000) 638 | continue 639 | } 640 | concurrency++ 641 | const { id, parent } = file 642 | const target = mapping[parent] || root 643 | copy_file(id, target, service_account, null, task_id).then(new_file => { 644 | if (new_file) { 645 | count++ 646 | db.prepare('INSERT INTO copied (taskid, fileid) VALUES (?, ?)').run(task_id, id) 647 | } 648 | }).catch(e => { 649 | err = e 650 | }).finally(() => { 651 | concurrency-- 652 | }) 653 | } while (concurrency || files.length) 654 | clearInterval(loop) 655 | if (err) throw err 656 | // const limit = pLimit(PARALLEL_LIMIT) 657 | // let count = 0 658 | // const loop = setInterval(() => { 659 | // const now = dayjs().format('HH:mm:ss') 660 | // const {activeCount, pendingCount} = limit 661 | // const message = `${now} | Number of files copied ${count} | Ongoing ${activeCount} | Pending ${pendingCount}` 662 | // print_progress(message) 663 | // }, 1000) 664 | // May cause excessive memory usage and be forced to exit by node 665 | // return Promise.all(files.map(async file => { 666 | // const { id, parent } = file 667 | // const target = mapping[parent] || root 668 | // const new_file = await limit(() => copy_file(id, target, service_account, limit, task_id)) 669 | // if (new_file) { 670 | // count++ 671 | // db.prepare('INSERT INTO copied (taskid, fileid) VALUES (?, ?)').run(task_id, id) 672 | // } 673 | // })).finally(() => clearInterval(loop)) 674 | } 675 | 676 | async function copy_file (id, parent, use_sa, limit, task_id) { 677 | let url = `https://www.googleapis.com/drive/v3/files/${id}/copy` 678 | let params = { supportsAllDrives: true } 679 | url += '?' + params_to_query(params) 680 | const config = {} 681 | let retry = 0 682 | while (retry < RETRY_LIMIT) { 683 | let gtoken 684 | if (use_sa) { 685 | const temp = await get_sa_token() 686 | gtoken = temp.gtoken 687 | config.headers = { authorization: 'Bearer ' + temp.access_token } 688 | } else { 689 | config.headers = await gen_headers() 690 | } 691 | try { 692 | const { data } = await axins.post(url, { parents: [parent] }, config) 693 | if (gtoken) gtoken.exceed_count = 0 694 | return data 695 | } catch (err) { 696 | retry++ 697 | handle_error(err) 698 | const data = err && err.response && err.response.data 699 | const message = data && data.error && data.error.message 700 | if (message && message.toLowerCase().includes('file limit')) { 701 | if (limit) limit.clearQueue() 702 | if (task_id) db.prepare('update task set status=? where id=?').run('error', task_id) 703 | throw new Error(FILE_EXCEED_MSG) 704 | } 705 | if (!use_sa && message && message.toLowerCase().includes('rate limit')) { 706 | throw new Error('Personal Drive Limit:' + message) 707 | } 708 | if (use_sa && message && message.toLowerCase().includes('rate limit')) { 709 | retry-- 710 | if (gtoken.exceed_count >= EXCEED_LIMIT) { 711 | SA_TOKENS = SA_TOKENS.filter(v => v.gtoken !== gtoken) 712 | if (!SA_TOKENS.length) SA_TOKENS = get_sa_batch() 713 | console.log(`This account has triggered the daily usage limit${EXCEED_LIMIT} consecutive times, the remaining amount of SA available in this batch:`, SA_TOKENS.length) 714 | } else { 715 | // console.log('This account triggers its daily usage limit and has been marked. If the next request is normal, it will be unmarked, otherwise the SA will be removed') 716 | if (gtoken.exceed_count) { 717 | gtoken.exceed_count++ 718 | } else { 719 | gtoken.exceed_count = 1 720 | } 721 | } 722 | } 723 | } 724 | } 725 | if (use_sa && !SA_TOKENS.length) { 726 | if (limit) limit.clearQueue() 727 | if (task_id) db.prepare('update task set status=? where id=?').run('error', task_id) 728 | throw new Error('All SA are exhausted') 729 | } else { 730 | console.warn('File creation failed,Fileid: ' + id) 731 | } 732 | } 733 | 734 | async function create_folders ({ source, old_mapping, folders, root, task_id, service_account }) { 735 | if (argv.dncf) return {} // do not copy folders 736 | if (!Array.isArray(folders)) throw new Error('folders must be Array:' + folders) 737 | const mapping = old_mapping || {} 738 | mapping[source] = root 739 | if (!folders.length) return mapping 740 | 741 | const missed_folders = folders.filter(v => !mapping[v.id]) 742 | console.log('Start copying folders, total:', missed_folders.length) 743 | const limit = pLimit(PARALLEL_LIMIT) 744 | let count = 0 745 | let same_levels = folders.filter(v => v.parent === folders[0].parent) 746 | 747 | const loop = setInterval(() => { 748 | const now = dayjs().format('HH:mm:ss') 749 | const message = `${now} | Folders Created ${count} | Ongoing ${limit.activeCount} | Pending ${limit.pendingCount}` 750 | print_progress(message) 751 | }, 1000) 752 | 753 | while (same_levels.length) { 754 | const same_levels_missed = same_levels.filter(v => !mapping[v.id]) 755 | await Promise.all(same_levels_missed.map(async v => { 756 | try { 757 | const { name, id, parent } = v 758 | const target = mapping[parent] || root 759 | const new_folder = await limit(() => create_folder(name, target, service_account, limit)) 760 | count++ 761 | mapping[id] = new_folder.id 762 | const mapping_record = id + ' ' + new_folder.id + '\n' 763 | db.prepare('update task set mapping = mapping || ? where id=?').run(mapping_record, task_id) 764 | } catch (e) { 765 | if (e.message === FILE_EXCEED_MSG) { 766 | clearInterval(loop) 767 | throw new Error(FILE_EXCEED_MSG) 768 | } 769 | console.error('Error creating Folder:', e.message) 770 | } 771 | })) 772 | // folders = folders.filter(v => !mapping[v.id]) 773 | same_levels = [].concat(...same_levels.map(v => folders.filter(vv => vv.parent === v.id))) 774 | } 775 | 776 | clearInterval(loop) 777 | return mapping 778 | } 779 | 780 | function find_dupe (arr) { 781 | const files = arr.filter(v => v.mimeType !== FOLDER_TYPE) 782 | const folders = arr.filter(v => v.mimeType === FOLDER_TYPE) 783 | const exists = {} 784 | const dupe_files = [] 785 | const dupe_folder_keys = {} 786 | for (const folder of folders) { 787 | const { parent, name } = folder 788 | const key = parent + '|' + name 789 | if (exists[key]) { 790 | dupe_folder_keys[key] = true 791 | } else { 792 | exists[key] = true 793 | } 794 | } 795 | const dupe_empty_folders = folders.filter(folder => { 796 | const { parent, name } = folder 797 | const key = parent + '|' + name 798 | return dupe_folder_keys[key] 799 | }).filter(folder => { 800 | const has_child = arr.some(v => v.parent === folder.id) 801 | return !has_child 802 | }) 803 | for (const file of files) { 804 | const { md5Checksum, parent, name } = file 805 | // Determining Duplicates based on file location and md5 value 806 | const key = parent + '|' + md5Checksum // + '|' + name 807 | if (exists[key]) { 808 | dupe_files.push(file) 809 | } else { 810 | exists[key] = true 811 | } 812 | } 813 | return dupe_files.concat(dupe_empty_folders) 814 | } 815 | 816 | async function confirm_dedupe ({ file_number, folder_number }) { 817 | const answer = await prompts({ 818 | type: 'select', 819 | name: 'value', 820 | message: `Duplicate files detected ${file_number},Empty Folders detected${folder_number},Delete them?`, 821 | choices: [ 822 | { title: 'Yes', description: 'confirm deletion', value: 'yes' }, 823 | { title: 'No', description: 'Donot delete', value: 'no' } 824 | ], 825 | initial: 0 826 | }) 827 | return answer.value 828 | } 829 | 830 | // Need sa to be the manager of the Teamdrive where the source folder is located 831 | async function mv_file ({ fid, new_parent, service_account }) { 832 | const file = await get_info_by_id(fid, service_account) 833 | if (!file) return 834 | const removeParents = file.parents[0] 835 | let url = `https://www.googleapis.com/drive/v3/files/${fid}` 836 | const params = { 837 | removeParents, 838 | supportsAllDrives: true, 839 | addParents: new_parent 840 | } 841 | url += '?' + params_to_query(params) 842 | const headers = await gen_headers(service_account) 843 | return axins.patch(url, {}, { headers }) 844 | } 845 | 846 | // To move files or folders to the recycle bin, SA should be content manager or above 847 | async function trash_file ({ fid, service_account }) { 848 | const url = `https://www.googleapis.com/drive/v3/files/${fid}?supportsAllDrives=true` 849 | const headers = await gen_headers(service_account) 850 | return axins.patch(url, { trashed: true }, { headers }) 851 | } 852 | 853 | // Delete files or folders directly without entering the recycle bin, requires SA as manager 854 | async function rm_file ({ fid, service_account }) { 855 | const headers = await gen_headers(service_account) 856 | let retry = 0 857 | const url = `https://www.googleapis.com/drive/v3/files/${fid}?supportsAllDrives=true` 858 | while (retry < RETRY_LIMIT) { 859 | try { 860 | return await axins.delete(url, { headers }) 861 | } catch (err) { 862 | retry++ 863 | handle_error(err) 864 | console.log('retrying to Delete, retry count', retry) 865 | } 866 | } 867 | } 868 | 869 | async function dedupe ({ fid, update, service_account, yes }) { 870 | let arr 871 | if (!update) { 872 | const info = get_all_by_fid(fid) 873 | if (info) { 874 | console.log('Locally cached data Found, cache time:', dayjs(info.mtime).format('YYYY-MM-DD HH:mm:ss')) 875 | arr = info 876 | } 877 | } 878 | arr = arr || await walk_and_save({ fid, update, service_account }) 879 | const dupes = find_dupe(arr) 880 | const folder_number = dupes.filter(v => v.mimeType === FOLDER_TYPE).length 881 | const file_number = dupes.length - folder_number 882 | const choice = yes || await confirm_dedupe({ file_number, folder_number }) 883 | if (choice === 'no') { 884 | return console.log('Exit') 885 | } else if (!choice) { 886 | return // ctrl+c 887 | } 888 | const limit = pLimit(PARALLEL_LIMIT) 889 | let folder_count = 0 890 | let file_count = 0 891 | await Promise.all(dupes.map(async v => { 892 | try { 893 | await limit(() => trash_file({ fid: v.id, service_account })) 894 | if (v.mimeType === FOLDER_TYPE) { 895 | console.log('Folder successfully deleted', v.name) 896 | folder_count++ 897 | } else { 898 | console.log('File successfully deleted', v.name) 899 | file_count++ 900 | } 901 | } catch (e) { 902 | console.log('Failed to delete', v) 903 | handle_error(e) 904 | } 905 | })) 906 | return { file_count, folder_count } 907 | } 908 | 909 | function handle_error (err) { 910 | const data = err && err.response && err.response.data 911 | if (data) { 912 | const message = data.error && data.error.message 913 | if (message && message.toLowerCase().includes('rate limit') && !argv.verbose) return 914 | console.error(JSON.stringify(data)) 915 | } else { 916 | if (!err.message.includes('timeout') || argv.verbose) console.error(err.message) 917 | } 918 | } 919 | 920 | function print_progress (msg) { 921 | if (process.stdout.cursorTo) { 922 | process.stdout.cursorTo(0) 923 | process.stdout.write(msg + ' ') 924 | } else { 925 | console.log(msg) 926 | } 927 | } 928 | 929 | 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 } 930 | -------------------------------------------------------------------------------- /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 | function is_pm2 () { 15 | return 'PM2_HOME' in process.env || 'PM2_JSON_PROCESSING' in process.env || 'PM2_CLI' in process.env 16 | } 17 | 18 | function is_int (n) { 19 | return n === parseInt(n) 20 | } 21 | 22 | router.get('/api/gdurl/count', async ctx => { 23 | if (!ROUTER_PASSKEY) return ctx.body = 'gd-utils Successfully started' 24 | const { query, headers } = ctx.request 25 | let { fid, type, update, passkey } = query 26 | if (passkey !== ROUTER_PASSKEY) return ctx.body = 'invalid passkey' 27 | if (!validate_fid(fid)) throw new Error('Invalid FolderID') 28 | 29 | let ua = headers['user-agent'] || '' 30 | ua = ua.toLowerCase() 31 | type = (type || '').toLowerCase() 32 | // todo type=tree 33 | if (!type) { 34 | if (ua.includes('curl')) { 35 | type = 'curl' 36 | } else if (ua.includes('mozilla')) { 37 | type = 'html' 38 | } else { 39 | type = 'json' 40 | } 41 | } 42 | if (type === 'html') { 43 | ctx.set('Content-Type', 'text/html; charset=utf-8') 44 | } else if (['json', 'all'].includes(type)) { 45 | ctx.set('Content-Type', 'application/json; charset=UTF-8') 46 | } 47 | ctx.body = await gen_count_body({ fid, type, update, service_account: true }) 48 | }) 49 | 50 | router.post('/api/gdurl/tgbot', async ctx => { 51 | const { body } = ctx.request 52 | console.log('ctx.ip', ctx.ip) // You can only allow the ip of the tg server 53 | console.log('tg message:', JSON.stringify(body, null, ' ')) 54 | if (TG_IPLIST && !TG_IPLIST.includes(ctx.ip)) return ctx.body = 'invalid ip' 55 | ctx.body = '' // Release the connection early 56 | const message = body.message || body.edited_message 57 | const message_str = JSON.stringify(message) 58 | 59 | const { callback_query } = body 60 | if (callback_query) { 61 | const { id, message, data } = callback_query 62 | const chat_id = callback_query.from.id 63 | const [action, fid, target] = data.split(' ').filter(v => v) 64 | if (action === 'count') { 65 | if (counting[fid]) return sm({ chat_id, text: fid + ' Counting, please wait a moment' }) 66 | counting[fid] = true 67 | send_count({ fid, chat_id }).catch(err => { 68 | console.error(err) 69 | sm({ chat_id, text: fid + ' Stats Failed:' + err.message }) 70 | }).finally(() => { 71 | delete counting[fid] 72 | }) 73 | } else if (action === 'copy') { 74 | if (COPYING_FIDS[fid + target]) return sm({ chat_id, text: 'Processing copy command with the same source and destination' }) 75 | COPYING_FIDS[fid + target] = true 76 | tg_copy({ fid, target: get_target_by_alias(target), chat_id }).then(task_id => { 77 | is_int(task_id) && sm({ chat_id, text: `Started Copying,TaskID: ${task_id} Type /task ${task_id} to know the Progress` }) 78 | }).finally(() => COPYING_FIDS[fid + target] = false) 79 | } else if (action === 'update') { 80 | if (counting[fid]) return sm({ chat_id, text: fid + ' Counting, please wait a moment' }) 81 | counting[fid] = true 82 | send_count({ fid, chat_id, update: true }).catch(err => { 83 | console.error(err) 84 | sm({ chat_id, text: fid + ' Stats Failed:' + err.message }) 85 | }).finally(() => { 86 | delete counting[fid] 87 | }) 88 | } else if (action === 'clear_button') { 89 | const { message_id, text } = message || {} 90 | if (message_id) sm({ chat_id, message_id, text, parse_mode: 'HTML' }, 'editMessageText') 91 | } 92 | return reply_cb_query({ id, data }).catch(console.error) 93 | } 94 | 95 | const chat_id = message && message.chat && message.chat.id 96 | const text = (message && message.text && message.text.trim()) || '' 97 | let username = message && message.from && message.from.username 98 | username = username && String(username).toLowerCase() 99 | let user_id = message && message.from && message.from.id 100 | user_id = user_id && String(user_id).toLowerCase() 101 | if (!chat_id || !tg_whitelist.some(v => { 102 | v = String(v).toLowerCase() 103 | return v === username || v === user_id 104 | })) { 105 | chat_id && sm({ chat_id, text: 'Fuck off,You are not supposed to Pm me you lil bitch, If you are the owner of this bot,then Please configure your username in config.js first' }) 106 | return console.warn('Received a request from a non-whitelisted user') 107 | } 108 | 109 | const fid = extract_fid(text) || extract_from_text(text) || extract_from_text(message_str) 110 | const no_fid_commands = ['/task', '/help', '/bm', '/reload'] 111 | if (!no_fid_commands.some(cmd => text.startsWith(cmd)) && !validate_fid(fid)) { 112 | return sm({ chat_id, text: 'Folder ID is invalid or not accessible' }) 113 | } 114 | if (text.startsWith('/help')) return send_help(chat_id) 115 | if (text.startsWith('/reload')) { 116 | if (!is_pm2()) return sm({ chat_id, text: 'Process is not a pm2 daemon,Do not restart' }) 117 | sm({ chat_id, text: 'Restart' }).then(() => process.exit()) 118 | } else if (text.startsWith('/bm')) { 119 | const [cmd, action, alias, target] = text.split(' ').map(v => v.trim()).filter(v => v) 120 | if (!action) return send_all_bookmarks(chat_id) 121 | if (action === 'set') { 122 | if (!alias || !target) return sm({ chat_id, text: 'Name and Destination FolderID cannot be empty ' }) 123 | if (alias.length > 24) return sm({ chat_id, text: 'Name Shouldnt be more than 24 Letters in Length' }) 124 | if (!validate_fid(target)) return sm({ chat_id, text: 'Incorrect Destination FolderID' }) 125 | set_bookmark({ chat_id, alias, target }) 126 | } else if (action === 'unset') { 127 | if (!alias) return sm({ chat_id, text: 'Name Cannot be empty' }) 128 | unset_bookmark({ chat_id, alias }) 129 | } else { 130 | send_bm_help(chat_id) 131 | } 132 | } else if (text.startsWith('/count')) { 133 | if (counting[fid]) return sm({ chat_id, text: fid + ' Counting, please wait a moment' }) 134 | try { 135 | counting[fid] = true 136 | const update = text.endsWith(' -u') 137 | await send_count({ fid, chat_id, update }) 138 | } catch (err) { 139 | console.error(err) 140 | sm({ chat_id, text: fid + ' Stats Failed:' + err.message }) 141 | } finally { 142 | delete counting[fid] 143 | } 144 | } else if (text.startsWith('/copy')) { 145 | let target = text.replace('/copy', '').replace(' -u', '').trim().split(' ').map(v => v.trim()).filter(v => v)[1] 146 | target = get_target_by_alias(target) || target 147 | if (target && !validate_fid(target)) return sm({ chat_id, text: `Destination FolderID ${target} is Invalid` }) 148 | if (COPYING_FIDS[fid + target]) return sm({ chat_id, text: 'Processing copy command with the same source and destination' }) 149 | COPYING_FIDS[fid + target] = true 150 | const update = text.endsWith(' -u') 151 | tg_copy({ fid, target, chat_id, update }).then(task_id => { 152 | is_int(task_id) && sm({ chat_id, text: `Started Copying,TaskID: ${task_id} Type /task ${task_id} To know the Progress` }) 153 | }).finally(() => COPYING_FIDS[fid + target] = false) 154 | } else if (text.startsWith('/task')) { 155 | let task_id = text.replace('/task', '').trim() 156 | if (task_id === 'all') { 157 | return send_all_tasks(chat_id) 158 | } else if (task_id === 'clear') { 159 | return clear_tasks(chat_id) 160 | } else if (task_id === '-h') { 161 | return send_task_help(chat_id) 162 | } else if (task_id.startsWith('rm')) { 163 | task_id = task_id.replace('rm', '') 164 | task_id = parseInt(task_id) 165 | if (!task_id) return send_task_help(chat_id) 166 | return rm_task({ task_id, chat_id }) 167 | } 168 | task_id = parseInt(task_id) 169 | if (!task_id) { 170 | const running_tasks = db.prepare('select id from task where status=?').all('copying') 171 | if (!running_tasks.length) return sm({ chat_id, text: 'There are currently no running tasks' }) 172 | return running_tasks.forEach(v => send_task_info({ chat_id, task_id: v.id }).catch(console.error)) 173 | } 174 | send_task_info({ task_id, chat_id }).catch(console.error) 175 | } else if (message_str.includes('drive.google.com/') || validate_fid(text)) { 176 | return send_choice({ fid: fid || text, chat_id }) 177 | } else { 178 | sm({ chat_id, text: 'This command is not currently supported' }) 179 | } 180 | }) 181 | 182 | module.exports = router 183 | -------------------------------------------------------------------------------- /src/snap2html.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const dayjs = require('dayjs') 4 | 5 | const ID_DIR_MAPPING = {} 6 | /* 7 | Data format: 8 | Each index in "dirs" array is an array representing a directory: 9 | First item: "directory path*always 0*directory modified date" 10 | Note that forward slashes are used instead of (Windows style) backslashes 11 | Then, for each each file in the directory: "filename*size of file*file modified date" 12 | Seconds to last item tells the total size of directory content 13 | Last item refrences IDs to all subdirectories of this dir (if any). 14 | ID is the item index in dirs array. 15 | const dirs = [ 16 | [ 17 | `C:/WordPress/wp-admin*0*1597318033`, 18 | `widgets.php*18175*1597318033`, 19 | 743642, 20 | // "2*11*12*13*14*15*16" 21 | "1" 22 | ], 23 | [ 24 | `C:/WordPress/wp-admin/test*0*1597318033`, 25 | `test.php*12175*1597318033`, 26 | 12175, 27 | "" 28 | ] 29 | ] */ 30 | 31 | function snap2html ({ root, data }) { 32 | const total_size = sum_size(data) 33 | const template = fs.readFileSync(path.join(__dirname, '../static/snap2html.template'), 'utf8') 34 | let html = template.replace('var dirs = []', 'var dirs = ' + JSON.stringify(trans(data, root))) 35 | html = html.replace(/\[TITLE\]/g, root.name) 36 | html = html.replace('[GEN DATE]', dayjs().format('YYYY-MM-DD HH:mm:ss')) 37 | const file_numbers = data.filter(v => !is_folder(v)).length 38 | const folder_numbers = data.filter(v => is_folder(v)).length 39 | html = html.replace(/\[NUM FILES\]/g, file_numbers) 40 | html = html.replace('[NUM DIRS]', folder_numbers) 41 | html = html.replace('[TOT SIZE]', total_size) 42 | return html 43 | } 44 | 45 | function sum_size (arr) { 46 | let total = 0 47 | arr.forEach(v => total += Number(v.size) || 0) 48 | return total 49 | } 50 | 51 | function is_folder (v) { 52 | return v.mimeType === 'application/vnd.google-apps.folder' 53 | } 54 | 55 | function unix_time (t) { 56 | if (!t) return 0 57 | t = +new Date(t) 58 | return parseInt(t / 1000, 10) 59 | } 60 | 61 | function escape_name (name) { 62 | return name.replace(/\*/g, '*') 63 | } 64 | 65 | function trans (arr, root) { 66 | if (!arr.length) return arr 67 | const first = arr[0] 68 | get_size(root, arr) 69 | let dirs = arr.filter(is_folder) 70 | dirs.unshift(root) 71 | dirs = dirs.map(dir => { 72 | const { name, id, size, modifiedTime } = dir 73 | const dir_path = root.name + get_path(id, arr) 74 | let result = [`${escape_name(dir_path)}*0*${unix_time(modifiedTime)}`] 75 | const children = arr.filter(v => v.parent === id) 76 | const child_files = children.filter(v => !is_folder(v)).map(file => { 77 | return `${escape_name(file.name)}*${file.size}*${unix_time(file.modifiedTime)}` 78 | }) 79 | result = result.concat(child_files) 80 | result.push(size) 81 | const sub_folders = children.filter(is_folder).map(v => dirs.findIndex(vv => vv.id === v.id)) 82 | result.push(sub_folders.join('*')) 83 | return result 84 | }) 85 | return dirs 86 | } 87 | 88 | function get_size (node, arr) { 89 | if (node.size !== undefined) return node.size 90 | const children = arr.filter(v => v.parent === node.id) 91 | const sizes = children.map(child => get_size(child, arr)) 92 | const total_size = sizes.reduce((acc, val) => Number(acc) + Number(val), 0) 93 | return node.size = total_size 94 | } 95 | 96 | function get_path (id, folders) { 97 | let result = ID_DIR_MAPPING[id] 98 | if (result !== undefined) return result 99 | result = '' 100 | let temp = id 101 | let folder = folders.filter(v => v.id === temp)[0] 102 | while (folder) { 103 | result = `/${folder.name}` + result 104 | temp = folder.parent 105 | if (ID_DIR_MAPPING[temp]) { 106 | result = ID_DIR_MAPPING[temp] + result 107 | return ID_DIR_MAPPING[id] = result 108 | } 109 | folder = folders.filter(v => v.id === temp)[0] 110 | } 111 | return ID_DIR_MAPPING[id] = result 112 | } 113 | 114 | module.exports = { snap2html } 115 | -------------------------------------------------------------------------------- /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 = ['Type', 'Number', 'Size'] 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 = ['Total', 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', 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 }, limit) { 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 === 'Folder') v.ext = '[Folder]' 60 | if (v.ext === 'No Extension') v.ext = '[NoExt]' 61 | }) 62 | let records = details.map(v => [v.ext, v.count, v.size]).map(arr => arr.map(content => ({ content, hAlign }))) 63 | const folder_row = records.pop() 64 | if (limit) records = records.slice(0, limit) 65 | if (folder_row) records.push(folder_row) 66 | const total_count = file_count + folder_count 67 | const tails = ['Total', total_count, total_size].map(v => ({ content: v, hAlign })) 68 | tb.push(headers, ...records) 69 | tb.push(tails) 70 | return tb.toString().replace(/─/g, '—') // Prevent the table from breaking on the mobile phone and it will look more beautiful in pc after removing the replace 71 | } 72 | 73 | function summary (info, sort_by) { 74 | const files = info.filter(v => v.mimeType !== 'application/vnd.google-apps.folder') 75 | const file_count = files.length 76 | const folder_count = info.filter(v => v.mimeType === 'application/vnd.google-apps.folder').length 77 | let total_size = info.map(v => Number(v.size) || 0).reduce((acc, val) => acc + val, 0) 78 | total_size = format_size(total_size) 79 | const exts = {} 80 | const sizes = {} 81 | let no_ext = 0; let no_ext_size = 0 82 | files.forEach(v => { 83 | let { name, size } = v 84 | size = Number(size) || 0 85 | const ext = name.split('.').pop().toLowerCase() 86 | if (!name.includes('.') || ext.length > 10) { // If there are more than 10 characters after . , it is judged as no extension 87 | no_ext_size += size 88 | return no_ext++ 89 | } 90 | if (exts[ext]) { 91 | exts[ext]++ 92 | } else { 93 | exts[ext] = 1 94 | } 95 | if (sizes[ext]) { 96 | sizes[ext] += size 97 | } else { 98 | sizes[ext] = size 99 | } 100 | }) 101 | const details = Object.keys(exts).map(ext => { 102 | const count = exts[ext] 103 | const size = sizes[ext] 104 | return { ext, count, size: format_size(size), raw_size: size } 105 | }) 106 | if (sort_by === 'size') { 107 | details.sort((a, b) => b.raw_size - a.raw_size) 108 | } else if (sort_by === 'name') { 109 | details.sort((a, b) => (a.ext > b.ext) ? 1 : -1) 110 | } else { 111 | details.sort((a, b) => b.count - a.count) 112 | } 113 | if (no_ext) details.push({ ext: 'No Extension', count: no_ext, size: format_size(no_ext_size), raw_size: no_ext_size }) 114 | if (folder_count) details.push({ ext: 'Folder', count: folder_count, size: 0, raw_size: 0 }) 115 | return { file_count, folder_count, total_size, details } 116 | } 117 | 118 | function format_size (n) { 119 | n = Number(n) 120 | if (Number.isNaN(n)) return '' 121 | if (n < 0) return 'invalid size' 122 | const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] 123 | let flag = 0 124 | while (n >= 1024) { 125 | n = (n / 1024) 126 | flag++ 127 | } 128 | return n.toFixed(2) + ' ' + units[flag] 129 | } 130 | -------------------------------------------------------------------------------- /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('Please set Bot_token in config.js first') 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 = `
[Information]
 27 | Command | Description
 28 | =====================
 29 | /reload | Restart the Task
 30 | =====================
 31 | /count FolderID [-u] | Shows Size
 32 | You can provide drive link too instead of FolderID
 33 | adding -u at the end is optional 
 34 | =====================
 35 | /copy sourceID DestID [-u] | Copy Files(Will create a New Folder)
 36 | If targetID is not filled in, it will be copied to the default location (set in config.js)
 37 | adding -u at the end is optional
 38 | =====================
 39 | /task | Shows info about the running task
 40 | Example:
 41 | /task | Shows info about the running task
 42 | /task 7 | Shows details of task number 7
 43 | /task all | Details of all records
 44 | /task clear | Clear al records
 45 | /task rm 7 | Clear Task number 7
 46 | =====================
 47 | /bm [action] [alias] [target] | bookmark,Add a common FolderID
 48 | Helpful while copying to same destination folder multiple times。
 49 | Eg:
 50 | /bm | Shows all bookmarks
 51 | /bm set movie folder-id | Add a Bookmark by the name movie
 52 | /bm unset movie | Delete this bookmark
 53 | 
` 54 | return sm({ chat_id, text, parse_mode: 'HTML' }) 55 | } 56 | 57 | function send_bm_help (chat_id) { 58 | const text = `
/bm [action] [alias] [target] | bookmark,Add a common FolderID
 59 | Helpful while copying to same destination folder multiple times。
 60 | Eg:
 61 | /bm | Shows all bookmarks
 62 | /bm set movie folder-id | Add a Bookmark by the name movie
 63 | /bm unset movie | Delete this bookmark
 64 | 
` 65 | return sm({ chat_id, text, parse_mode: 'HTML' }) 66 | } 67 | 68 | function send_task_help (chat_id) { 69 | const text = `
/task | Shows info about the running task
 70 | Example:
 71 | /task | Shows info about the running task
 72 | /task 7 | Shows details of task number 7
 73 | /task all | Details of all records
 74 | /task clear | Clear al records
 75 | /task rm 7 | Clear Task number 7
 76 | 
` 77 | return sm({ chat_id, text, parse_mode: 'HTML' }) 78 | } 79 | 80 | function clear_tasks (chat_id) { 81 | const finished_tasks = db.prepare('select id from task where status=?').all('finished') 82 | finished_tasks.forEach(task => rm_task({ task_id: task.id })) 83 | sm({ chat_id, text: 'All completed tasks have been cleared' }) 84 | } 85 | 86 | function rm_task ({ task_id, chat_id }) { 87 | const exist = db.prepare('select id from task where id=?').get(task_id) 88 | if (!exist) return sm({ chat_id, text: `Task ${task_id} doesnt exist 😀` }) 89 | db.prepare('delete from task where id=?').run(task_id) 90 | db.prepare('delete from copied where taskid=?').run(task_id) 91 | if (chat_id) sm({ chat_id, text: `Task ${task_id} Deleted` }) 92 | } 93 | 94 | function send_all_bookmarks (chat_id) { 95 | let records = db.prepare('select alias, target from bookmark').all() 96 | if (!records.length) return sm({ chat_id, text: 'No Bookmarks Found' }) 97 | const tb = new Table({ style: { head: [], border: [] } }) 98 | const headers = ['Name', 'FolderID'] 99 | records = records.map(v => [v.alias, v.target]) 100 | tb.push(headers, ...records) 101 | const text = tb.toString().replace(/─/g, '—') 102 | return sm({ chat_id, text: `
${text}
`, parse_mode: 'HTML' }) 103 | } 104 | 105 | function set_bookmark ({ chat_id, alias, target }) { 106 | const record = db.prepare('select alias from bookmark where alias=?').get(alias) 107 | if (record) return sm({ chat_id, text: 'There is anothe Favourite Folder with the same name' }) 108 | db.prepare('INSERT INTO bookmark (alias, target) VALUES (?, ?)').run(alias, target) 109 | return sm({ chat_id, text: `Bookmark Succesfully Set 💌:${alias} | ${target}` }) 110 | } 111 | 112 | function unset_bookmark ({ chat_id, alias }) { 113 | const record = db.prepare('select alias from bookmark where alias=?').get(alias) 114 | if (!record) return sm({ chat_id, text: 'No Bookmarks found with this Name' }) 115 | db.prepare('delete from bookmark where alias=?').run(alias) 116 | return sm({ chat_id, text: 'Bookmark succesfully deleted 😕 ' + alias }) 117 | } 118 | 119 | function get_target_by_alias (alias) { 120 | const record = db.prepare('select target from bookmark where alias=?').get(alias) 121 | return record && record.target 122 | } 123 | 124 | function get_alias_by_target (target) { 125 | const record = db.prepare('select alias from bookmark where target=?').get(target) 126 | return record && record.alias 127 | } 128 | 129 | function send_choice ({ fid, chat_id }) { 130 | return sm({ 131 | chat_id, 132 | text: `The FolderID ${fid} is Identified,Choose what would you like to do`, 133 | reply_markup: { 134 | inline_keyboard: [ 135 | [ 136 | { text: 'Calculate Size', callback_data: `count ${fid}` }, 137 | { text: 'Copy', callback_data: `copy ${fid}` } 138 | ], 139 | [ 140 | { text: 'Refresh', callback_data: `update ${fid}` }, 141 | { text: 'Clear', callback_data: `clear_button` } 142 | ] 143 | ].concat(gen_bookmark_choices(fid)) 144 | } 145 | }) 146 | } 147 | 148 | // console.log(gen_bookmark_choices()) 149 | function gen_bookmark_choices (fid) { 150 | const gen_choice = v => ({ text: `Copy to ${v.alias}`, callback_data: `copy ${fid} ${v.alias}` }) 151 | const records = db.prepare('select * from bookmark').all() 152 | const result = [] 153 | for (let i = 0; i < records.length; i += 2) { 154 | const line = [gen_choice(records[i])] 155 | if (records[i + 1]) line.push(gen_choice(records[i + 1])) 156 | result.push(line) 157 | } 158 | return result 159 | } 160 | 161 | async function send_all_tasks (chat_id) { 162 | let records = db.prepare('select id, status, ctime from task').all() 163 | if (!records.length) return sm({ chat_id, text: 'No task record in the database' }) 164 | const tb = new Table({ style: { head: [], border: [] } }) 165 | const headers = ['ID', 'status', 'ctime'] 166 | records = records.map(v => { 167 | const { id, status, ctime } = v 168 | return [id, status, dayjs(ctime).format('YYYY-MM-DD HH:mm:ss')] 169 | }) 170 | tb.push(headers, ...records) 171 | const text = tb.toString().replace(/─/g, '—') 172 | const url = `https://api.telegram.org/bot${tg_token}/sendMessage` 173 | return axins.post(url, { 174 | chat_id, 175 | parse_mode: 'HTML', 176 | text: `All Copy Tasks:\n
${text}
` 177 | }).catch(err => { 178 | console.error(err.message) 179 | // const description = err.response && err.response.data && err.response.data.description 180 | // if (description && description.includes('message is too long')) { 181 | const text = [headers].concat(records.slice(-100)).map(v => v.join('\t')).join('\n') 182 | return sm({ chat_id, parse_mode: 'HTML', text: `All copy tasks (The last 100):\n
${text}
` }) 183 | }) 184 | } 185 | 186 | async function get_task_info (task_id) { 187 | const record = db.prepare('select * from task where id=?').get(task_id) 188 | if (!record) return {} 189 | const { source, target, status, mapping, ctime, ftime } = record 190 | const { copied_files } = db.prepare('select count(fileid) as copied_files from copied where taskid=?').get(task_id) 191 | const folder_mapping = mapping && mapping.trim().split('\n') 192 | const new_folder = folder_mapping && folder_mapping[0].split(' ')[1] 193 | const { summary } = db.prepare('select summary from gd where fid=?').get(source) || {} 194 | const { file_count, folder_count, total_size } = summary ? JSON.parse(summary) : {} 195 | const total_count = (file_count || 0) + (folder_count || 0) 196 | const copied_folders = folder_mapping ? (folder_mapping.length - 1) : 0 197 | let text = 'Task No:' + task_id + '\n' 198 | const folder_name = await get_folder_name(source) 199 | text += 'Source Folder:' + gen_link(source, folder_name) + '\n' 200 | text += 'Destination Folder:' + gen_link(target, get_alias_by_target(target)) + '\n' 201 | text += 'New Folder:' + (new_folder ? gen_link(new_folder) : 'Not Created yet') + '\n' 202 | text += 'Task Status:' + status + '\n' 203 | text += 'Start Time:' + dayjs(ctime).format('YYYY-MM-DD HH:mm:ss') + '\n' 204 | text += 'End Time:' + (ftime ? dayjs(ftime).format('YYYY-MM-DD HH:mm:ss') : 'Not Done') + '\n' 205 | text += 'Folder Progress:' + copied_folders + '/' + (folder_count === undefined ? 'Unknown' : folder_count) + '\n' 206 | text += 'File Progress:' + copied_files + '/' + (file_count === undefined ? 'Unkno wn' : file_count) + '\n' 207 | text += 'Total Percentage:' + ((copied_files + copied_folders) * 100 / total_count).toFixed(2) + '%\n' 208 | text += 'Total Size:' + (total_size || 'Unknown') 209 | return { text, status, folder_count } 210 | } 211 | 212 | async function send_task_info ({ task_id, chat_id }) { 213 | const { text, status, folder_count } = await get_task_info(task_id) 214 | if (!text) return sm({ chat_id, text: 'he task ID does not exist in the database:' + task_id }) 215 | const url = `https://api.telegram.org/bot${tg_token}/sendMessage` 216 | let message_id 217 | try { 218 | const { data } = await axins.post(url, { chat_id, text, parse_mode: 'HTML' }) 219 | message_id = data && data.result && data.result.message_id 220 | } catch (e) { 221 | console.log('fail to send message to tg', e.message) 222 | } 223 | // get_task_info crash cpu when the number of Folders is too large,In the future, it is better to save the mapping as a separate table 224 | if (!message_id || status !== 'copying') return 225 | const loop = setInterval(async () => { 226 | const { text, status } = await get_task_info(task_id) 227 | // TODO check if text changed 228 | if (status !== 'copying') clearInterval(loop) 229 | sm({ chat_id, message_id, text, parse_mode: 'HTML' }, 'editMessageText') 230 | }, 10 * 1000) 231 | } 232 | 233 | async function tg_copy ({ fid, target, chat_id, update }) { // return task_id 234 | target = target || DEFAULT_TARGET 235 | if (!target) return sm({ chat_id, text: 'Please enter the destination ID or set the default copy destination ID in config.js first(DEFAULT_TARGET)' }) 236 | 237 | const file = await get_info_by_id(fid, !USE_PERSONAL_AUTH) 238 | if (!file) { 239 | const text = `Unable to get info,Please check if the link is valid and the SAs have appropriate permissions:https://drive.google.com/drive/folders/${fid}` 240 | return sm({ chat_id, text }) 241 | } 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: `Copied the File succesfully,File:${gen_link(target)}` }) 245 | }).catch(e => { 246 | sm({ chat_id, text: `Failed to copy the File,Reason:${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 | return sm({ chat_id, text: 'Same Task alreading in Progress,Check it here /task ' + record.id }) 254 | } else if (record.status === 'finished') { 255 | sm({ chat_id, text: `Existing Task detected ${record.id},Start copying` }) 256 | } 257 | } 258 | 259 | real_copy({ source: fid, update, target, service_account: !USE_PERSONAL_AUTH, is_server: true }) 260 | .then(async info => { 261 | if (!record) record = {} // Prevent infinite loop 262 | if (!info) return 263 | const { task_id } = info 264 | const { text } = await get_task_info(task_id) 265 | sm({ chat_id, text, parse_mode: 'HTML' }) 266 | }) 267 | .catch(err => { 268 | const task_id = record && record.id 269 | if (task_id) db.prepare('update task set status=? where id=?').run('error', task_id) 270 | if (!record) record = {} 271 | console.error('Copy Failed', fid, '-->', target) 272 | console.error(err) 273 | sm({ chat_id, text: (task_id || '') + 'Error,Reason:' + err.message }) 274 | }) 275 | 276 | while (!record) { 277 | record = db.prepare('select id from task where source=? and target=?').get(fid, target) 278 | await sleep(1000) 279 | } 280 | return record.id 281 | } 282 | 283 | function sleep (ms) { 284 | return new Promise((resolve, reject) => { 285 | setTimeout(resolve, ms) 286 | }) 287 | } 288 | 289 | function reply_cb_query ({ id, data }) { 290 | const url = `https://api.telegram.org/bot${tg_token}/answerCallbackQuery` 291 | return axins.post(url, { 292 | callback_query_id: id, 293 | text: 'Start the Task ' + data 294 | }) 295 | } 296 | 297 | async function send_count ({ fid, chat_id, update }) { 298 | const gen_text = payload => { 299 | const { obj_count, processing_count, pending_count } = payload || {} 300 | const now = dayjs().format('YYYY-MM-DD HH:mm:ss') 301 | return `Size:${gen_link(fid)} 302 | Time:${now} 303 | Number of Files:${obj_count || ''} 304 | ${pending_count ? ('Pending:' + pending_count) : ''} 305 | ${processing_count ? ('Ongoing:' + processing_count) : ''}` 306 | } 307 | 308 | const url = `https://api.telegram.org/bot${tg_token}/sendMessage` 309 | let response 310 | try { 311 | response = await axins.post(url, { chat_id, text: `Collecting info about ${fid},Please wait,It is recommended not to start copying before this gets over` }) 312 | } catch (e) {} 313 | const { data } = response || {} 314 | const message_id = data && data.result && data.result.message_id 315 | const message_updater = payload => sm({ 316 | chat_id, 317 | message_id, 318 | parse_mode: 'HTML', 319 | text: gen_text(payload) 320 | }, 'editMessageText') 321 | 322 | const service_account = !USE_PERSONAL_AUTH 323 | const table = await gen_count_body({ fid, update, service_account, type: 'tg', tg: message_id && message_updater }) 324 | if (!table) return sm({ chat_id, parse_mode: 'HTML', text: gen_link(fid) + ' Failed to obtain info' }) 325 | const gd_link = `https://drive.google.com/drive/folders/${fid}` 326 | const name = await get_folder_name(fid) 327 | return axins.post(url, { 328 | chat_id, 329 | parse_mode: 'HTML', 330 | text: `
Source Folder Name:${name}
331 | Source Folder Link:${gd_link}
332 | ${table}
` 333 | }).catch(async err => { 334 | console.log(err.message) 335 | // const description = err.response && err.response.data && err.response.data.description 336 | // const too_long_msgs = ['request entity too large', 'message is too long'] 337 | // if (description && too_long_msgs.some(v => description.toLowerCase().includes(v))) { 338 | const limit = 20 339 | const table = await gen_count_body({ fid, type: 'tg', service_account: !USE_PERSONAL_AUTH, limit }) 340 | return sm({ 341 | chat_id, 342 | parse_mode: 'HTML', 343 | text: `
Source Folder Name:${name}
344 | Source Folder Link:${gd_link}
345 | The table is too long and exceeds the telegram message limit, only the first ${limit} will be displayed:
346 | ${table}
` 347 | }) 348 | }) 349 | } 350 | 351 | function sm (data, endpoint) { 352 | endpoint = endpoint || 'sendMessage' 353 | const url = `https://api.telegram.org/bot${tg_token}/${endpoint}` 354 | return axins.post(url, data).catch(err => { 355 | // console.error('fail to post', url, data) 356 | console.error('fail to send message to tg:', err.message) 357 | const err_data = err.response && err.response.data 358 | err_data && console.error(err_data) 359 | }) 360 | } 361 | 362 | function extract_fid (text) { 363 | text = text.replace(/^\/count/, '').replace(/^\/copy/, '').replace(/\\n/g, '').replace(/\\/g, '').trim() 364 | const [source, target] = text.split(' ').map(v => v.trim()) 365 | if (validate_fid(source)) return source 366 | try { 367 | if (!text.startsWith('http')) text = 'https://' + text 368 | const u = new URL(text) 369 | if (u.pathname.includes('/folders/')) { 370 | return u.pathname.split('/').map(v => v.trim()).filter(v => v).pop() 371 | } else if (u.pathname.includes('/file/')) { 372 | const file_reg = /file\/d\/([a-zA-Z0-9_-]+)/ 373 | const file_match = u.pathname.match(file_reg) 374 | return file_match && file_match[1] 375 | } 376 | return u.searchParams.get('id') 377 | } catch (e) { 378 | return '' 379 | } 380 | } 381 | 382 | function extract_from_text (text) { 383 | // const reg = /https?:\/\/drive.google.com\/[^\s]+/g 384 | const reg = /https?:\/\/drive.google.com\/[a-zA-Z0-9_\\/?=&-]+/g 385 | const m = text.match(reg) 386 | return m && extract_fid(m[0]) 387 | } 388 | 389 | 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 } 390 | -------------------------------------------------------------------------------- /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: `/Root Folder [Total${files.length} Files (excluding folders) , ${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} | [Total${count}Files ${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/roshanconnor123/gd-utils/f09caa7557ce862c5017f8700af0c30c589b3e7c/static/autorclone.png -------------------------------------------------------------------------------- /static/choose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roshanconnor123/gd-utils/f09caa7557ce862c5017f8700af0c30c589b3e7c/static/choose.png -------------------------------------------------------------------------------- /static/colab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roshanconnor123/gd-utils/f09caa7557ce862c5017f8700af0c30c589b3e7c/static/colab.png -------------------------------------------------------------------------------- /static/count.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roshanconnor123/gd-utils/f09caa7557ce862c5017f8700af0c30c589b3e7c/static/count.png -------------------------------------------------------------------------------- /static/error-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roshanconnor123/gd-utils/f09caa7557ce862c5017f8700af0c30c589b3e7c/static/error-log.png -------------------------------------------------------------------------------- /static/gclone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roshanconnor123/gd-utils/f09caa7557ce862c5017f8700af0c30c589b3e7c/static/gclone.png -------------------------------------------------------------------------------- /static/gdurl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roshanconnor123/gd-utils/f09caa7557ce862c5017f8700af0c30c589b3e7c/static/gdurl.png -------------------------------------------------------------------------------- /static/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roshanconnor123/gd-utils/f09caa7557ce862c5017f8700af0c30c589b3e7c/static/tree.png -------------------------------------------------------------------------------- /validate-sa.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { argv } = require('yargs') 4 | .usage('Usage: ./$0 folder-id\nfolder-id Does SA has read Pemission to the directory ID you want to detect') 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('Start testing', SA_TOKENS.length, 'SA accounts') 32 | const invalid_sa = await get_invalid_sa(SA_TOKENS, fid) 33 | if (!invalid_sa.length) return console.log('Detected', SA_TOKENS.length, 'Individual SA,No invalid account detected') 34 | const choice = await choose(invalid_sa.length) 35 | if (choice === 'yes') { 36 | mv_sa(invalid_sa) 37 | console.log('Successfully moved') 38 | } else { 39 | console.log('Successful exit, invalid SA record:', invalid_sa) 40 | } 41 | } else { 42 | console.warn('Folder ID is missing or malformed') 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 | async function choose (count) { 55 | const answer = await prompts({ 56 | type: 'select', 57 | name: 'value', 58 | message: `Detcted ${count} Invalid SA,Whether to move them to the sa/invalid Folder?`, 59 | choices: [ 60 | { title: 'Yes', description: 'Confirm Move', value: 'yes' }, 61 | { title: 'No', description: 'Exit without making changes', value: 'no' } 62 | ], 63 | initial: 0 64 | }) 65 | return answer.value 66 | } 67 | 68 | async function get_invalid_sa (arr, fid) { 69 | if (!fid) throw new Error('Please specify the ID of the directory to check permissions') 70 | const fails = [] 71 | let flag = 0 72 | let good = 0 73 | for (const v of arr) { 74 | console.log('Inspection Progress', `${flag++}/${arr.length}`) 75 | console.log('Normal/Abnormal', `${good}/${fails.length}`) 76 | const {gtoken, filename} = v 77 | try { 78 | const access_token = await get_sa_token(gtoken) 79 | await get_info(fid, access_token) 80 | good++ 81 | } catch (e) { 82 | handle_error(e) 83 | const status = e && e.response && e.response.status 84 | if (Number(status) === 400) fails.push(filename) // access_token Failed 85 | 86 | const data = e && e.response && e.response.data 87 | const code = data && data.error && data.error.code 88 | if ([404, 403].includes(Number(code))) fails.push(filename) // Failed to read folder information 89 | } 90 | } 91 | return fails 92 | } 93 | 94 | function handle_error (err) { 95 | const data = err && err.response && err.response.data 96 | if (data) { 97 | console.error(JSON.stringify(data)) 98 | } else { 99 | console.error(err.message) 100 | } 101 | } 102 | 103 | async function get_info (fid, access_token) { 104 | let url = `https://www.googleapis.com/drive/v3/files/${fid}` 105 | let params = { 106 | includeItemsFromAllDrives: true, 107 | supportsAllDrives: true, 108 | corpora: 'allDrives', 109 | fields: 'id,name' 110 | } 111 | url += '?' + params_to_query(params) 112 | const headers = { authorization: 'Bearer ' + access_token } 113 | const { data } = await axins.get(url, { headers }) 114 | return data 115 | } 116 | 117 | function params_to_query (data) { 118 | const ret = [] 119 | for (let d in data) { 120 | ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d])) 121 | } 122 | return ret.join('&') 123 | } 124 | 125 | async function get_sa_token (gtoken) { 126 | return new Promise((resolve, reject) => { 127 | gtoken.getToken((err, tk) => { 128 | err ? reject(err) : resolve(tk.access_token) 129 | }) 130 | }) 131 | } 132 | 133 | function validate_fid (fid) { 134 | if (!fid) return false 135 | fid = String(fid) 136 | if (fid.length < 10 || fid.length > 100) return false 137 | const reg = /^[a-zA-Z0-9_-]+$/ 138 | return fid.match(reg) 139 | } 140 | --------------------------------------------------------------------------------