branch: elpa/hyperdrive commit ad6c7b90abf6748a8c47ea0d9db27233a55dd503 Merge: 0d04b9b2ea 2f1aebbe04 Author: Joseph Turner <jos...@ushin.org> Commit: Joseph Turner <jos...@ushin.org>
Add hyperdrive-forget-file command --- CHANGELOG.org | 2 + doc/hyperdrive.org | 18 +++++++ doc/hyperdrive.texi | 22 ++++++++ hyperdrive-diff.el | 2 + hyperdrive-dir.el | 1 + hyperdrive-lib.el | 153 ++++++++++++++++++++++++++++------------------------ hyperdrive-menu.el | 6 ++- hyperdrive-vars.el | 2 +- hyperdrive.el | 100 ++++++++++++++++++++++++---------- 9 files changed, 206 insertions(+), 100 deletions(-) diff --git a/CHANGELOG.org b/CHANGELOG.org index aa026e75d1..b98a26a2ba 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -28,6 +28,8 @@ installation with ~M-x hyperdrive-install~, and a faster directory UI! - When visiting an old version of a hyperdrive file, press ~n~ and ~p~ to traverse the version history. Press ~q~ to kill the current buffer. - View hyperdrive disk usage with ~hyperdrive-describe~ and ~hyperdrive-menu~. +- Delete the local copy of a file or directory with + ~hyperdrive-forget-file~, also bound in ~hyperdrive-menu~ and menu bar. ** Changed diff --git a/doc/hyperdrive.org b/doc/hyperdrive.org index 260e172406..06419a74f3 100644 --- a/doc/hyperdrive.org +++ b/doc/hyperdrive.org @@ -267,6 +267,10 @@ available inside the directory view by default: Delete the file or directory (recursively) at point. +- Key: F (hyperdrive-forget-file) :: + + Delete your local copy of the file for the current buffer. + - Key: H (hyperdrive-dir-history) :: Open the version history (see [[*View the hyperdrive version history]]) @@ -457,6 +461,20 @@ version of the hyperdrive (see [[*View the hyperdrive version history]]).* This command also has a keybinding in the directory view (see [[*Directory view]]). +** Forget a hyperdrive file + +It is possible to "forget" your local copy of a hyperdrive file in +order to save disk space. "Forgetting" a file does not delete the +file from the hyperdrive and does not increment the hyperdrive's +version number. + +- Command: hyperdrive-forget-file :: + + Delete your local copy of the file for the current buffer. + +This command also has a keybinding in the directory view (see +[[*Directory view]]). + ** View the hyperdrive version history Hyperdrives are versioned, meaning that you can explore the history of diff --git a/doc/hyperdrive.texi b/doc/hyperdrive.texi index ceba76ef70..4572a6e60b 100644 --- a/doc/hyperdrive.texi +++ b/doc/hyperdrive.texi @@ -84,6 +84,7 @@ Usage * Write to a hyperdrive:: * Link to a hyperdrive:: * Delete a hyperdrive file:: +* Forget a hyperdrive file:: * View the hyperdrive version history:: * Describe a hyperdrive:: * Name a hyperdrive:: @@ -286,6 +287,7 @@ On the network it still may be there. * Write to a hyperdrive:: * Link to a hyperdrive:: * Delete a hyperdrive file:: +* Forget a hyperdrive file:: * View the hyperdrive version history:: * Describe a hyperdrive:: * Name a hyperdrive:: @@ -502,6 +504,11 @@ Download the file at point to disk. @findex hyperdrive-delete Delete the file or directory (recursively) at point. +@item @kbd{F} (@code{hyperdrive-forget-file}) +@kindex F +@findex hyperdrive-forget-file +Delete your local copy of the file for the current buffer. + @item @kbd{H} (@code{hyperdrive-dir-history}) @kindex H @findex hyperdrive-dir-history @@ -728,6 +735,21 @@ Delete the hyperdrive file in the current buffer. This command also has a keybinding in the directory view (see @ref{Directory view}). +@node Forget a hyperdrive file +@section Forget a hyperdrive file + +It is possible to ``forget'' your local copy of a hyperdrive file in +order to save disk space. ``Forgetting'' a file does not delete the +file from the hyperdrive and does not increment the hyperdrive's +version number. + +@deffn Command hyperdrive-forget-file +Delete your local copy of the file for the current buffer. +@end deffn + +This command also has a keybinding in the directory view (see +@ref{Directory view}). + @node View the hyperdrive version history @section View the hyperdrive version history diff --git a/hyperdrive-diff.el b/hyperdrive-diff.el index 44caa5b1a7..9885d00aa5 100644 --- a/hyperdrive-diff.el +++ b/hyperdrive-diff.el @@ -106,10 +106,12 @@ This function is intended to diff files, not directories." (h/api 'get (he/url old-entry) :queue queue :as 'response :else #'ignore :then (lambda (response) + (h//fill old-entry (plz-response-headers response)) (setf old-response response))) (h/api 'get (he/url new-entry) :queue queue :as 'response :else #'ignore :then (lambda (response) + (h//fill new-entry (plz-response-headers response)) (setf new-response response))))) ;;;; Mode diff --git a/hyperdrive-dir.el b/hyperdrive-dir.el index ef3b0f3df9..b379422f6b 100644 --- a/hyperdrive-dir.el +++ b/hyperdrive-dir.el @@ -217,6 +217,7 @@ With point on header, returns directory entry." "d" #'h/download "^" #'h/up "D" #'h/delete + "F" #'h/forget-file "H" #'h/dir-history "s" #'h/dir-sort "?" #'h/menu diff --git a/hyperdrive-lib.el b/hyperdrive-lib.el index 00ffcbcd3c..c2ff1f70d2 100644 --- a/hyperdrive-lib.el +++ b/hyperdrive-lib.el @@ -166,11 +166,13 @@ REST may include the argument `:queue', a `plz-queue' in which to make the request." ;; TODO: Document that the request/queue is returned. ;; TODO: Should we create a wrapper for `h/api' which calls - ;; `h//fill-latest-version' for requests to - ;; directories/requests which modify the drive (and therefore - ;; always return the latest version number). If we did this, we - ;; could remove redundant calls to - ;; `h//fill-latest-version' everywhere else. + ;; `h//fill-latest-version' for requests to directories/requests which modify + ;; the drive (and therefore always return the latest version number). If we + ;; did this, we could remove redundant calls to `h//fill-latest-version' + ;; everywhere else. X-Drive-Size is returned by many types of requests, and it + ;; would simplify the code to handle updating the hyperdrive disk-usage in one + ;; place. Once implemented, go through each call to `h/api' to verify that + ;; disk-usage is updated correctly. (declare (indent defun)) (pcase method ((and (or 'get 'head) @@ -466,7 +468,7 @@ When VERSION is nil, return latest version of ENTRY." (setf (he/version entry) version) (condition-case err ;; FIXME: Requests to out of range version currently hang. - (h/fill entry :then 'sync) + (h/fill entry) (plz-error (pcase (plz-response-status (plz-error-response (caddr err))) ;; FIXME: If plz-error is a curl-error, this block will fail. @@ -620,7 +622,7 @@ echo area when the request for the file is made." (when messagep (h/message "Opening <%s>..." (he/url entry))))) -(cl-defun h/fill (entry &key queue then else) +(cl-defun h/fill (entry &key queue (then 'sync) else) "Fill ENTRY's metadata and call THEN. If THEN is `sync', return the filled entry and ignore ELSE. Otherwise, make request asynchronously and call THEN with the @@ -723,17 +725,18 @@ Returns filled ENTRY." ;; but no public-key. (cl-pushnew domain (h/domains (he/hyperdrive entry)) :test #'equal))) (setf (h/public-key hyperdrive) public-key)) - (if (and (h//entry-directory-p entry) - (null (he/version entry))) - ;; Version-less directory HEAD/GET request ETag header always have the - ;; hyperdrive's latest version. We don't currently store - ;; version ranges for directories (since they don't - ;; technically have versions in hyperdrive). - (h//fill-latest-version hyperdrive headers) - ;; File HEAD/GET request ETag header does not retrieve the - ;; hyperdrive's latest version, so `h/update-existent-version-range' - ;; will not necessarily fill in the entry's last range. - (h/update-existent-version-range entry (string-to-number etag))) + (when etag + (if (and (h//entry-directory-p entry) + (null (he/version entry))) + ;; Version-less directory HEAD/GET request ETag header always have the + ;; hyperdrive's latest version. We don't currently store + ;; version ranges for directories (since they don't + ;; technically have versions in hyperdrive). + (h//fill-latest-version hyperdrive headers) + ;; File HEAD/GET request ETag header does not retrieve the + ;; hyperdrive's latest version, so `h/update-existent-version-range' + ;; will not necessarily fill in the entry's last range. + (h/update-existent-version-range entry (string-to-number etag)))) entry)) (defun h//fill-listing-entries (listing hyperdrive version) @@ -966,14 +969,18 @@ HYPERDRIVE's public metadata file." ;; NOTE: Don't attempt to fill hyperdrive struct with old metadata :version nil)) (metadata (condition-case err - (h/api 'get (he/url entry) - :as (lambda () - (condition-case nil - (json-read) - (json-error - (h/message "Error parsing JSON metadata file: %s" - (he/url entry))))) - :noquery t) + ;; TODO: Refactor to use :as 'response-with-buffer and call h/fill + (pcase-let + (((cl-struct plz-response headers body) + (h/api 'get (he/url entry) :as 'response :noquery t))) + (h//fill entry headers) + (with-temp-buffer + (insert body) + (goto-char (point-min)) + (json-read))) + (json-error + (h/message "Error parsing JSON metadata file: %s" + (he/url entry))) (plz-error (pcase (plz-response-status (plz-error-response (caddr err))) ;; FIXME: If plz-error is a curl-error, this block will fail. @@ -1314,13 +1321,15 @@ With PURGE, delete hash table entry for HYPERDRIVE." That is, if the SEED has been used to create a local hyperdrive." (condition-case err - (pcase (h/api 'get (format "hyper://localhost/?key=%s" - (url-hexify-string seed)) - :as 'response :noquery t) - ((and (pred plz-response-p) - response - (guard (= 200 (plz-response-status response)))) - (plz-response-body response))) + (pcase-let + (((cl-struct plz-response (body url)) + (h/api 'get (format "hyper://localhost/?key=%s" + (url-hexify-string seed)) + :as 'response :noquery t))) + ;; TODO: Update hyperdrive disk-usage. The following doesn't work + ;; because the response doesn't have the proper ETag header: + ;; (h//fill (h/url-entry url) headers) + url) (plz-error (if (= 400 (plz-response-status (plz-error-response (caddr err)))) ;; FIXME: If plz-error is a curl-error, this block will fail. nil @@ -1348,47 +1357,51 @@ Otherwise, return nil. SLOT may be one of (cl-defun h/handler-default (entry &key then) "Load ENTRY's file into an Emacs buffer. If then, then call THEN with no arguments. Default handler." - (h/api 'get (he/url entry) - :noquery t - :as (lambda () - (pcase-let* - (((cl-struct hyperdrive-entry hyperdrive version etc) entry) - ((map target) etc) - (response-buffer (current-buffer))) - (with-current-buffer (h//get-buffer-create entry) - ;; TODO: Don't reload if we're jumping to a link on the - ;; same page (but ensure that reverting still works). - (if (buffer-modified-p) - (h/message "Buffer modified: %S" (current-buffer)) - (save-excursion - (with-silent-modifications - (erase-buffer) - (insert-buffer-substring response-buffer)) - (setf buffer-undo-list nil) - (setf buffer-read-only - (or (not (h/writablep hyperdrive)) version)) - (set-buffer-modified-p nil) - (set-visited-file-modtime (current-time)))) - (when (map-elt (hyperdrive-etc hyperdrive) 'safep) - (let ((buffer-file-name (he/name entry))) - (set-auto-mode))) - (when target - (pcase major-mode - ('org-mode - (require 'hyperdrive-org) - (h/org--link-goto target)) - ('markdown-mode - ;; TODO: Handle markdown link - ))) - (h/blob-mode (if version +1 -1)) - (when then - (funcall then))))))) + (pcase-let* + (((cl-struct plz-response headers body) + ;; TODO: Handle errors + ;; TODO: When plz adds :as 'response-with-buffer, use that. + (h/api 'get (he/url entry) :noquery t :as 'response)) + ;; Filling entry is necessary in order to update hyperdrive disk-usage. + (_ (h//fill entry headers)) + ((cl-struct hyperdrive-entry hyperdrive version etc) entry) + ((map target) etc)) + (with-current-buffer (h//get-buffer-create entry) + ;; TODO: Don't reload if we're jumping to a link on the + ;; same page (but ensure that reverting still works). + (if (buffer-modified-p) + (h/message "Buffer modified: %S" (current-buffer)) + (save-excursion + (with-silent-modifications + (erase-buffer) + (insert body)) + (setf buffer-undo-list nil) + (setf buffer-read-only + (or (not (h/writablep hyperdrive)) version)) + (set-buffer-modified-p nil) + (set-visited-file-modtime (current-time)))) + (when (map-elt (hyperdrive-etc hyperdrive) 'safep) + (let ((buffer-file-name (he/name entry))) + (set-auto-mode))) + (when target + (pcase major-mode + ('org-mode + (require 'hyperdrive-org) + (h/org--link-goto target)) + ('markdown-mode + ;; TODO: Handle markdown link + ))) + (h/blob-mode (if version +1 -1)) + (when then + (funcall then))))) (cl-defun h/handler-streamable (entry &key _then) ;; TODO: Is there any reason to not pass THEN through? "Stream ENTRY." - (h/message "Streaming %s..." (h//format-entry-url entry)) - (pcase-let ((`(,command . ,args) (split-string h/stream-player-command))) + ;; NOTE: Since data is streamed to an external process, disk usage will not be + ;; updated until a later request. + (h/message "Streaming %s..." (h//format-entry-url entry)) + (pcase-let ((`(,command . ,args) (split-string h/stream-player-command))) (apply #'start-process "hyperdrive-stream-player" nil command (cl-substitute (h//httpify-url (he/url entry)) diff --git a/hyperdrive-menu.el b/hyperdrive-menu.el index adefba958c..7a7164f95b 100644 --- a/hyperdrive-menu.el +++ b/hyperdrive-menu.el @@ -71,7 +71,7 @@ "Hyperdrive")) ("h" "Hyperdrive" h/menu-hyperdrive) ("N" "New drive" h/new) - ("L" "Open Link" h/open-url)] + ("L" "Open link" h/open-url)] ["Version" :if (lambda () (and (h/menu--scope) @@ -166,7 +166,9 @@ (h/menu--scope))) (or version (not (h/writablep hyperdrive)))))) ("d" "Download" h/download - :if-not-mode h/dir-mode)] + :if-not-mode h/dir-mode) + ("F" "Forget file" h/forget-file + :transient t)] ;; TODO: Consider adding a defcustom to hide the "Selected" and ;; "Current" groups when in a directory buffer. [;; Selected diff --git a/hyperdrive-vars.el b/hyperdrive-vars.el index 31bf171e53..2c9c26ebef 100644 --- a/hyperdrive-vars.el +++ b/hyperdrive-vars.el @@ -380,7 +380,7 @@ values are alists mapping version range starts to plists with ;;;;; Internals (defvar h/gateway-version-expected - '(:name "hyper-gateway-ushin" :version "3.8.0")) + '(:name "hyper-gateway-ushin" :version "3.9.2")) (defvar h/gateway-version-checked-p nil "Non-nil if the gateway's version has been checked. diff --git a/hyperdrive.el b/hyperdrive.el index 561c5947c9..65213e96e8 100644 --- a/hyperdrive.el +++ b/hyperdrive.el @@ -135,19 +135,16 @@ Return version if gateway is running; otherwise signal an error." If SEED is not currently used as the petname for another hyperdrive, the new hyperdrive's petname will be set to SEED." (interactive (list (h/read-name :prompt "New hyperdrive seed"))) - (let* ((response (h/api 'post (concat "hyper://localhost/?key=" (url-hexify-string seed)))) - (url (progn - ;; NOTE: Working around issue in plz whereby the - ;; stderr process sentinel sometimes leaves "stderr - ;; finished" garbage in the response body in older - ;; Emacs versions. See: <https://github.com/alphapapa/plz.el/issues/23>. - (string-match (rx bos (group "hyper://" (1+ nonl))) response) - (match-string 1 response))) - (hyperdrive (he/hyperdrive (h/url-entry url)))) + (pcase-let* (((cl-struct plz-response (body url)) + (h/api 'post (concat "hyper://localhost/?key=" + (url-hexify-string seed)) + :as 'response)) + (hyperdrive (he/hyperdrive (h/url-entry url)))) (setf (h/seed hyperdrive) seed) (setf (h/writablep hyperdrive) t) (unwind-protect (h/set-petname seed hyperdrive) + ;; TODO: Hyperdrive disk usage should be set here. (h/persist hyperdrive) (h/open (h/url-entry url))))) @@ -185,6 +182,33 @@ Interactively, prompt for hyperdrive and action." (propertize "safe" 'face 'success) (propertize "unsafe" 'face 'error)))))) +(defun h/forget-file (entry) + "Delete local copy of the file or directory contents of ENTRY. +Only delete the blob(s) for the file or directory at ENTRY's +version; other versions of the file or directory are not cleared. +If ENTRY is a directory, recursively delete blobs for all files +within the directory. Hyperdrive directory contents are not +modified; file blobs may be recoverable from other peers." + ;; TODO: Consider supporting an :all-versions key for clearing the cache for + ;; all versions of the file/directory. + (interactive (list (h//context-entry))) + (when (yes-or-no-p + (format-message + "Clear local copy of entry (data may not be recoverable—see manual):`%s'? " + (h//format-entry entry))) + (let ((url (he/url entry))) + (h/api 'post url + :headers '(("Cache-Control" . "no-store")) + :as 'response + :else (lambda (err) + (h/error "Unable to clear cache for `%s': %S" url err)) + :then (lambda (response) + (h//fill entry (plz-response-headers response)) + (h/message "Cleared `%s'" (h//format-entry entry)) + ;; TODO: When file sizes in hyperdrive-dir-mode are colorized + ;; based locally downloaded sizes, refresh ewoc entry here. + ))))) + ;;;###autoload (defun hyperdrive-purge (hyperdrive) "Purge all data corresponding to HYPERDRIVE." @@ -449,7 +473,11 @@ in a directory. Otherwise, or with universal prefix argument (when (file-exists-p filename) ;; plz.el will not overwrite existing files: ensure there's no file there. (delete-file filename)) - (h/api 'get url :as `(file ,filename)))) + (h/api 'get url :as `(file ,filename)) + ;; TODO: If plz adds support for getting response headers when downloading + ;; as a file, use it here. + ;; Filling entry is necessary in order to update hyperdrive disk-usage. + (h/fill (h/url-entry url)))) ;;;###autoload (defun hyperdrive-write-buffer (entry &optional overwritep) @@ -696,6 +724,7 @@ After successful upload, call THEN. When QUEUE, use it." :body `(file ,filename) :headers `(("Last-Modified" . ,last-modified)) :then then) + ;; TODO: Hyperdrive disk usage should be set here. (unless queue (h/message "Uploading to \"%s\"..." url)))) @@ -776,6 +805,8 @@ Universal prefix argument \\[universal-argument] forces (require 'url) +;; TODO: EWW buffers end up being marked as modified, and Emacs prompts to save +;; them before exiting. Emacs should not prompt to save *eww* buffers. (defun h/url-loader (parsed-url) "Retrieve URL synchronously. PARSED-URL must be a URL-struct like the output of @@ -783,12 +814,17 @@ PARSED-URL must be a URL-struct like the output of The return value of this function is the retrieval buffer." (cl-check-type parsed-url url "Need a pre-parsed URL.") - (let* ((url (url-recreate-url parsed-url)) - ;; response-buffer will contain the loaded HTML, and will be deleted at the end of `eww-render'. - (response-buffer (h/api 'get url :as 'buffer))) - (with-current-buffer response-buffer + (pcase-let* ((url (url-recreate-url parsed-url)) + ;; TODO: When `plz.el' adds :as 'response-with-buffer, use that. + ;; response-buffer will contain the loaded HTML, and will be deleted at the end of `eww-render'. + ((cl-struct plz-response headers body) + (h/api 'get url :as 'response))) + ;; Filling entry is necessary in order to update hyperdrive disk-usage. + (h//fill (h/url-entry url) headers) + (with-current-buffer (generate-new-buffer " *hyperdrive-eww*") (widen) (goto-char (point-min)) + (insert body) (while (search-forward (string ?\C-m) nil t) ;; Strip CRLF from headers so that `eww-parse-headers' works correctly. ;; MAYBE: As an alternative, look at buffer coding systems to @@ -1205,7 +1241,12 @@ The return value of this function is the retrieval buffer." ;; TODO: Add `hyperdrive--parent-entry-p' (not (string= ".." (alist-get 'display-name (he/etc selected-entry)))))) - :help "Delete file/directory at point"]) + :help "Delete file/directory at point"] + ["Forget file" (lambda () + (interactive) + (call-interactively #'h/forget-file)) + :help "Delete local copy of file/directory contents at point"] + ) ("Version" :label (let* ((version (he/version h/current-entry)) (existsp (he/exists-p h/current-entry)) @@ -1356,21 +1397,26 @@ Intended for relative (i.e. non-full) URLs." ;;;;; Installation (defvar h/gateway-urls-and-hashes + ;; TODO: sr.ht build (<https://builds.sr.ht/~ushin/job/1247130#task-setup>) + ;; fail due to a kernel issue: https://github.com/nodejs/node/issues/53051 '((gnu/linux - ( :url "https://codeberg.org/USHIN/hyper-gateway-ushin/releases/download/v3.8.0/hyper-gateway-ushin-linux" - :sha256 "8ff669bd378e88a3c80d65861f4088071852afaedf7bba56c88c1a162ed9e4f3") - ( :url "https://git.sr.ht/~ushin/hyper-gateway-ushin/refs/download/v3.8.0/hyper-gateway-linux-v3.8.0" - :sha256 "")) + ( :url "https://codeberg.org/USHIN/hyper-gateway-ushin/releases/download/v3.9.2/hyper-gateway-ushin-linux" + :sha256 "f0cb3e793b3d27ce159e8e034e03b5a14cbdc53d47bd8f0761310792d5b6a7aa") + ;; ( :url "https://git.sr.ht/~ushin/hyper-gateway-ushin/refs/download/v3.9.2/hyper-gateway-linux-v3.9.2" + ;; :sha256 "331dbc0048decd42d197667f96aabdaf25306ba4e7ba0451dd9a2f31868fa86c") + ) (darwin - ( :url "https://codeberg.org/USHIN/hyper-gateway-ushin/releases/download/v3.8.0/hyper-gateway-ushin-macos" - :sha256 "22f6131f48d740f429690f16baac19b20a2211250360a89580db95415398d03c") - ( :url "https://git.sr.ht/~ushin/hyper-gateway-ushin/refs/download/v3.8.0/hyper-gateway-macos-v3.8.0" - :sha256 "")) + ( :url "https://codeberg.org/USHIN/hyper-gateway-ushin/releases/download/v3.9.2/hyper-gateway-ushin-macos" + :sha256 "bb472bf7a536eb30bc2443ce90cfca1bf2aa71177afdc1377f4fc9b61414c24c") + ;; ( :url "https://git.sr.ht/~ushin/hyper-gateway-ushin/refs/download/v3.9.2/hyper-gateway-macos-v3.9.2" + ;; :sha256 "e78d3c1394774fc49212d86827eb615d46ae1a04c82fc0328ac31bbbdb201aa0") + ) (windows-nt - ( :url "https://codeberg.org/USHIN/hyper-gateway-ushin/releases/download/v3.8.0/hyper-gateway-ushin-windows.exe" - :sha256 "c347255d3fc5e6499fc10bea4d20e62798fb5968960dbbe26d507d11688326bb") - ( :url "https://git.sr.ht/~ushin/hyper-gateway-ushin/refs/download/v3.8.0/hyper-gateway-windows-v3.8.0.exe" - :sha256 ""))) + ( :url "https://codeberg.org/USHIN/hyper-gateway-ushin/releases/download/v3.9.2/hyper-gateway-ushin-windows.exe" + :sha256 "7a72010cd7bc1b0357673838f5ccb069e58bf3c229bc873bedd0ee9faa805188") + ;; ( :url "https://git.sr.ht/~ushin/hyper-gateway-ushin/refs/download/v3.9.2/hyper-gateway-windows-v3.9.2.exe" + ;; :sha256 "d4fa29aca473148e2d13215d042e4be40657080035caa2d3a699b741b6a45845") + )) "Alist mapping `system-type' to URLs where the gateway can be downloaded.") ;;;###autoload