写了一个异步下载/解压/拷贝文件的包:async-copy-file


#1

其实就是把命令行包装了一下,免去每次拼装命令行的麻烦。在 Emacs 中无非就是下载/拷贝文件到某处,解压然后跳过顶层目录。所以我把命令分成三组,按需组合:

  • copy (cp)
  • download (curl)
  • extract (tar, unzip)

都是 macOS1 和 Linux 内置的命令(Windows Powershell 应该也有这些/类似的命令,但是我不懂怎么使用 ╮(╯_╰)╭ 所以就空着以后再填了)。

使用方法:

(async-copy-file
 url-or-local-file                        ;; 根据输入的文件名自动选用 copy / download
 output-dir

 ;; 以下是可选参数

 :overwrite t                             ;; 覆盖已存在文件

 :extract-arg '(unzip :strip-component 1) ;; 使用 unzip 解压文件 (如果是 tar.xx 格式的则选用 tar),
                                          ;; 并去掉第 1 级目录。

 ;; :dry-run t                            ;; 打印最终命令,而非执行

 :finish-fn                               ;; 命令执行完后的回调
 (lambda (output-buffer)
   (kill-buffer output-buffer)
   (message "==> Finish callback")))

我原本也想只用 Emacs 内置函数来实现,但是 url-copy-file 看不到下载进度让人有点着急,我还是习惯弹出一个 *output* 缓冲,看着下载进度比较踏实(如果实在不想看到这个 *output* 也是有办法屏蔽的)。

而且 url-copy-file 是先下载到内存,完成之后才写入目标文件,遇到大文件恐怕不太靠谱(我看看 async-copy-file 是不是可以给 curl 命令加上断点续传的参数)。

最关键的是仍然绕不开命令行,我稍早就写了一个 url-copy-file 函数 + async + unzip 命令的包:https://github.com/twlz0ne/unzip.el


1 macOS 下的 tar 其实是 bsdtar,支持 zip 解压,但是我并没有使用这项功能,为了和 Linux 保持一致。


UPDATE

除了弹窗显示进度,也可以考虑在 modeline 转圈圈提示,具体参考 #14 楼


#2

GNU工具比BSD本家残疾还真是少见,难道是zip的意识形态问题?

dired-aux.el有个dired-compress-file,可以解压/压缩文件,也许可以省点功夫。


#3

我也喜欢进度条。url-copy-file 应该可以实现进度条,要下载的文件大小一般是预先知道的,已经下载的大小也是知道的,有机会折腾看看。

对,这也是个痛点,如果要用它下载了 Debian DVD 文件那就惨了,即便 Emacs 不卡死,你的电脑也至少要有 3.6 GB 的空闲内存,因为 Emacs 要把这 3.6 GB 的内容写到一个 Buffer 中,想到于 Emacs 直接打开这个文件。


还有 url-copy-file 是同步的,应该用 url.el 写一个异步的,用 url-retrieve ?命令 package-refresh-contents 就支持异步,但印象中 url.el 不是很靠谱,Emacs 作为编辑器,是一个用户程序,不像专门的编程语言、环境那么可靠。


#4

有libcurl的dyn module,不过十分简陋


#5

用异步的 shell-command 或者直接 async-shell-command 也有进度条

M-! curl -O https://mirrors.tuna.tsinghua.edu.cn/gnu/emacs/emacs-26.2.tar.gz &

#6

试了下

(async-copy-file "http://example.com/index.html" "index.html")

完成的时候报告

error in process sentinel: funcall: Symbol's function definition is void: nil
error in process sentinel: Symbol's function definition is void: nil

finish-fn 没有写,是必填参数?


#7

finish-fn 是可选参数。

已经改好了。


#8

我看 url-copy-file 没啥好改的,除非改它底层的 url-retrieve-synchronously 函数。 url-retrieve-synchronously 倒是有打印「进度」,不过输出在调试信息中:

    (setq asynch-buffer
	  (url-retrieve url (lambda (&rest ignored)
			      (url-debug 'retrieval "Synchronous fetching done (%S)" (current-buffer))
			      (setq retrieval-done t
				    asynch-buffer (current-buffer)))
			nil silent inhibit-cookies))

文件大小可以从 HTTP 头字段 Content-Length 中得到。


#9

这个是结束的时候打印一次。要显示进度条(百分比)应该需要了解更 url.el 底层的实现,估计比较困难。


#10

好东西,改天试试看。希望可以用到lsp-python-ms里


#11

lsp-python-ms,lsp-java,似乎是启动时ensure server然后下载安装。但是lsp目前支持不了异步启动。


#12

可以自己写个 lsp 启动函数和 ensure 函数,然后在回调函数里启动 lsp


#13

对我也正想说这个方案,但要控制下不循环调用就可以了。


#14

添加了一个不弹窗口的 async-copy-file-quiet 函数(仍然可以手动切换到 *async-copy-file* 查看命令行详细输出)。

然后通过 spinner 在 modeline 转圈圈提示:

(let ((curr-buffer (current-buffer)))
  (spinner-start)
  (async-copy-file-quiet
   (lsp-python-ms-latest-nupkg-url)
   "~/ms-pyls"
   :overwrite t
   :extract-arg 'unzip
   :finish-fn
   `(lambda (_)
      (with-current-buffer ,curr-buffer
        (spinner-stop))
     (message "Done"))))

虽然不是百分比进度提示,但至少表明「进程还在跑」。也许可以改造 spinner 实现百分比进度提示。


#15

我又想到了一个最小干扰的进度提示方法:lv-message

在 minibuffer 上面加一行,用来显示 *async-copy-file* 最后一行的内容。无论是下载、解压还是拷贝, *async-copy-file* 的最后一行都在滚动。


另一个方法是,用 Header Lines 显示进度。


#16

mode-line 上process应该也可以