emacs-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

features/user-directory 6f137f2: New library user-directory.el


From: Stefan Kangas
Subject: features/user-directory 6f137f2: New library user-directory.el
Date: Sun, 7 Nov 2021 13:50:26 -0500 (EST)

branch: features/user-directory
commit 6f137f29133aafd59bba3fd31777ca99189a87e5
Author: Stefan Kangas <stefan@marxist.se>
Commit: Stefan Kangas <stefan@marxist.se>

    New library user-directory.el
    
    * lisp/user-directory.el: New file.
    * test/lisp/user-directory-tests.el: New file.
---
 lisp/user-directory.el            | 263 ++++++++++++++++++++++++++++++++++++++
 test/lisp/user-directory-tests.el | 161 +++++++++++++++++++++++
 2 files changed, 424 insertions(+)

diff --git a/lisp/user-directory.el b/lisp/user-directory.el
new file mode 100644
index 0000000..72876bd
--- /dev/null
+++ b/lisp/user-directory.el
@@ -0,0 +1,263 @@
+;;; user-directory.el --- Find user-specific directories  -*- lexical-binding: 
t; -*-
+
+;; Copyright (C) 2021  Free Software Foundation, Inc.
+
+;; Author: Stefan Kangas <stefan@marxist.se>
+;; Keywords: internal
+;; Package: emacs
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; * Introduction
+;;
+;; This library contains functions to handle various user directories,
+;; such as configuration files, user data, etc. in a platform
+;; independent way.
+;;
+;; Users can override the returned directories with
+;; `user-directory-alist'.
+;;
+;; * Using from Lisp
+;;
+;; The entry point for Lisp libraries is `user-file' and
+;; `user-directory'.
+;;
+;; - User options for file names should be defined relative to the
+;;   paths returned by this library.
+;;
+;; - Instead of calling this once and caching the value in a variable,
+;;   best practice is to call it on use.  That way the user can update
+;;   the settings here without having to reload your library.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'xdg)
+
+(defgroup user-directory nil
+  "User directories."
+  :group 'environment
+  :version "29.1")
+
+(defcustom user-directory-alist ()
+  "Overrides for `user-directory'.
+This allows you to override where Emacs stores your configuration
+and data files."
+  :type 'list
+  :risky t)
+
+(defcustom user-directory-warning t
+  "If non-nil, `user-directory' will warn if unable to access or create 
directory.
+Set this to nil at your own risk, as it might lead to data loss
+when Emacs tries to save something in a non-existent or
+inaccessible location."
+  :type 'boolean)
+
+
+;;;; user-directory
+
+;;;###autoload
+(cl-defgeneric user-directory (type)
+  "Return a user-specific directory of TYPE.
+TYPE is one of these symbols:
+
+ - cache        Emacs cached files
+ - config       Emacs configuration files
+ - data         Emacs user data
+ - runtime      Emacs runtime files
+ - state        Emacs state data
+ - desktop      User desktop              (e.g. \"~/Desktop\")
+ - downloads    User downloaded files     (e.g. \"~/Downloads\")
+ - documents    User documents            (e.g. \"~/Documents\")
+ - music        User music files          (e.g. \"~/Music\")
+ - public       User shared files         (e.g. \"~/Public\")
+ - pictures     User picture files        (e.g. \"~/Pictures\")
+ - templates    User template files       (e.g. \"~/Templates\")
+ - videos       User video files          (e.g. \"~/Videos\")
+
+For more details, see below.")
+
+(defun user-directory--find-or-create-dir (dirs)
+  "Find the first directory that exists and is accessible in DIRS.
+Return value is (DIR . ERRTYPES)."
+  (let* (errtypes
+         (found
+          (catch 'found
+            (dolist (dir dirs)
+              ;; Make sure the top level directory exists, unless
+              ;; we're in batch mode or dumping Emacs.
+              (or noninteractive
+                  dump-mode
+                  (if (file-directory-p dir)
+                      (or (and (file-accessible-directory-p dir)
+                               (throw 'found (cons dir errtypes)))
+                          (push "access" errtypes))
+                    (with-file-modes #x700
+                      (condition-case nil
+                          (progn (make-directory dir t)
+                                 (setq errtypes nil)
+                                 (throw 'found (cons dir errtypes)))
+                        (error (push "create" errtypes))))))))))
+    (or found (cons nil errtypes))))
+
+(cl-defmethod user-directory :around (type)
+  (convert-standard-filename
+   (pcase-let* ((dirs (delq nil (cons (cdr (assq type user-directory-alist))
+                                      (cl-call-next-method))))
+                (`(,dir . ,errtypes) (user-directory--find-or-create-dir 
dirs)))
+     (when (and (not dir) user-directory-warning
+                (get 'user-directory-warning type))
+       ;; Warn only once per Emacs session and type.
+       (put 'user-directory-warning type t)
+       (display-warning 'initialization
+                        (format "\
+`user-directory' is unable to %s the %s user directory: %s
+Any data that would normally be written there may be lost!
+If you never want to see this message again,
+customize the variable `user-directory-warning'."
+                                ;; Warn about the most specific directory.
+                                (car errtypes) type (car dirs))))
+     ;; If no usable directory was found, return the most specific one.
+     (or dir (car dirs)))))
+
+
+;;;; Configuration, cache, and state.
+
+(cl-defmethod user-directory ((_type (eql 'cache)))
+  "Return the user cache directory.
+The cache directory contains non-essential user data that is not
+necessarily important to save.  "
+  (cons (expand-file-name "emacs" (xdg-cache-home))
+        '("~/.cache/emacs")))
+
+(cl-defmethod user-directory ((_type (eql 'config)))
+  "Return the user configuration file directory.
+The configuration file directory contains any user-specific
+configuration."
+  (cons (expand-file-name "emacs" (xdg-config-home))
+        '("~/.config/emacs")))
+
+(cl-defmethod user-directory ((_type (eql 'data)))
+  "Return the user data directory.
+Examples of things that belong in the user data directory are
+bookmarks and chat logs."
+  (cons (expand-file-name "emacs" (xdg-data-home))
+        '("~/.local/share/emacs")))
+
+(cl-defmethod user-directory ((_type (eql 'runtime)))
+  "Return the user runtime directory.
+The runtime directory contains user-specific non-essential
+runtime files and other file objects (such as sockets, named
+pipes, etc.)."
+  (list (cond ((expand-file-name "emacs" (xdg-runtime-dir)))
+              ((error "Unable to find runtime directory")))))
+
+(cl-defmethod user-directory ((_type (eql 'state)))
+  "Return the user state directory.
+The state directory contains user data that should persist
+between restarts of Emacs, but is not important enough to store
+in the data directory.  Things like completion history and lists
+of recently opened files probably belong here."
+  (cons (expand-file-name "emacs" (xdg-state-home))
+        '("~/.local/state/emacs")))
+
+
+;;;; User files.
+
+(cl-defmethod user-directory ((_type (eql 'desktop)))
+  "Return user desktop directory."
+  (cons (xdg-user-dir "DESKTOP")
+        '("~/Desktop")))
+
+(cl-defmethod user-directory ((_type (eql 'download)))
+  "Return user directory for downloads."
+  (cons (xdg-user-dir "DOWNLOAD")
+        '("~/Downloads")))
+
+(cl-defmethod user-directory ((_type (eql 'downloads)))
+  "Return user directory for downloads."
+  (cons (xdg-user-dir "DOWNLOAD")
+        '("~/Downloads")))
+
+(cl-defmethod user-directory ((_type (eql 'documents)))
+  "Return user directory for documents."
+  (cons (xdg-user-dir "DOCUMENTS")
+        '("~/Documents")))
+
+(cl-defmethod user-directory ((_type (eql 'music)))
+  "Return user directory for music."
+  (cons (xdg-user-dir "MUSIC")
+        '("~/Music")))
+
+(cl-defmethod user-directory ((_type (eql 'public)))
+  "Return user directory for public (shared) files."
+  (cons (xdg-user-dir "PUBLIC")
+        '("~/Public")))
+
+(cl-defmethod user-directory ((_type (eql 'pictures)))
+  "Return user directory for documents."
+  (cons (xdg-user-dir "PICTURES")
+        '("~/Pictures")))
+
+(cl-defmethod user-directory ((_type (eql 'templates)))
+  "Return user directory for templates."
+  (cons (xdg-user-dir "TEMPLATES")
+        '("~/Templates")))
+
+(cl-defmethod user-directory ((_type (eql 'videos)))
+  "Return user directory for video files."
+  (cons (xdg-user-dir "VIDEOS")
+        '("~/Videos")))
+
+
+;;;; user-file
+
+;;;###autoload
+(defun user-file (type name &optional old-name)
+  "Return an absolute per-user Emacs-specific file name.
+TYPE should be a symbol and is passed as an argument to
+`user-directory'.
+
+1. If NEW-NAME exists in the directory for TYPE, return it.
+
+2. Else if OLD-NAME is non-nil and OLD-NAME exists, return OLD-NAME.
+   OLD-NAME is an absolute file name or a list of absolute file
+   names.  If it is a list, try each of the names in the list.
+
+3. Else return NEW-NAME in the directory for TYPE, creating the
+   directory if it does not exist.  (Only the top level directory
+   for that type will be created, as with `user-directory'.)
+
+Note: in contrast with `locate-user-emacs-file', OLD-NAME is not
+a relative but an absolute file name.  This typically means that
+you will need to add an explicit \"~/\" at the beginning of the
+string, when converting calls from that function to this one."
+  (convert-standard-filename
+   (let* ((dir (user-directory type))
+          (new-name (abbreviate-file-name (expand-file-name name dir))))
+     (or (and old-name
+              (not (file-readable-p new-name))
+              (or (and (listp old-name)
+                       (car (seq-filter #'file-readable-p old-name)))
+                  (and (file-readable-p old-name)
+                       old-name)))
+         new-name))))
+
+(provide 'user-directory)
+
+;;; user-directory.el ends here
diff --git a/test/lisp/user-directory-tests.el 
b/test/lisp/user-directory-tests.el
new file mode 100644
index 0000000..f7a3f16
--- /dev/null
+++ b/test/lisp/user-directory-tests.el
@@ -0,0 +1,161 @@
+;;; user-directory-tests.el --- tests for user-directory.el -*- 
lexical-binding: t -*-
+
+;; Copyright (C) 2021 Free Software Foundation, Inc.
+
+;; Author: Stefan Kangas <stefan@marxist.se>
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert)
+(require 'ert-x)
+(require 'user-directory)
+
+(defmacro with-user-directory-test (&rest body)
+  (declare (indent defun) (debug (symbolp body)))
+  `(ert-with-temp-directory dir
+     (let ((user-directory-alist `((cache . ,dir)
+                     (config . ,dir)
+                     (data . ,dir)
+                     (runtime . ,dir)
+                     (state . ,dir)
+                     (desktop . ,dir)
+                     (downloads . ,dir)
+                     (documents . ,dir)
+                     (music . ,dir)
+                     (public . ,dir)
+                     (pictures . ,dir)
+                     (templates . ,dir)
+                     (videos . ,dir))))
+       ,@body)))
+
+
+;;;; user-directory
+
+(ert-deftest user-directory/returns-strings ()
+  (with-user-directory-test
+    (should (equal (user-directory 'cache) dir))
+    (should (equal (user-directory 'config) dir))
+    (should (equal (user-directory 'data) dir))
+    (should (equal (user-directory 'runtime) dir))
+    (should (equal (user-directory 'state) dir))
+    (should (equal (user-directory 'desktop) dir))
+    (should (equal (user-directory 'downloads) dir))
+    (should (equal (user-directory 'documents) dir))
+    (should (equal (user-directory 'music) dir))
+    (should (equal (user-directory 'public) dir))
+    (should (equal (user-directory 'pictures) dir))
+    (should (equal (user-directory 'templates) dir))
+    (should (equal (user-directory 'videos) dir))))
+
+(ert-deftest user-directory/creates-dir-if-missing ()
+  (with-user-directory-test
+    (delete-directory dir)
+    (user-directory 'downloads)
+    (should (file-exists-p dir))))
+
+(ert-deftest user-directory/alist-entry-overrides ()
+  (with-user-directory-test
+    (ert-with-temp-directory newdir
+      (let* ((user-directory-alist `((desktop . ,newdir))))
+        (unwind-protect
+            (should (equal (user-directory 'desktop) newdir))
+          (delete-directory newdir))))))
+
+(ert-deftest user-directory/alist-entry-overrides/inaccessible ()
+  (with-user-directory-test
+    (ert-with-temp-directory dir
+      (let* ((user-directory-alist `((desktop . ,dir))))
+        (unwind-protect
+            (progn
+              (chmod dir #x000)
+              (should-not (equal (user-directory 'desktop) dir)))
+          (delete-directory dir))))))
+
+
+;;;; user-file
+
+(ert-deftest user-directory-tests-user-file/name-exists ()
+  (with-user-directory-test
+    (should (string-match "\\`foo\\'"
+                          (file-name-base
+                           (user-file 'downloads "foo"))))))
+
+(ert-deftest user-directory-tests-user-file/name-missing ()
+  (with-user-directory-test
+    (should (string-match "\\`foo\\'"
+                          (file-name-base
+                           (user-file 'downloads "foo"))))))
+
+(ert-deftest user-directory-tests-user-file/old-missing ()
+  (with-user-directory-test
+    (let ((old-name (make-temp-name "/tmp/bar")))
+      (should (string-match "\\`foo\\'"
+                            (file-name-base
+                             (user-file 'downloads "foo" old-name)))))))
+
+(ert-deftest user-directory-tests-user-file/old-exists ()
+  (with-user-directory-test
+    (ert-with-temp-file old-name
+      (should (file-equal-p (user-file 'downloads "foo" old-name)
+                            old-name)))))
+
+(ert-deftest user-directory-tests-user-file/old-and-new-exists ()
+  (with-user-directory-test
+    (ert-with-temp-file old-name
+      (with-temp-file (expand-file-name "new-name"
+                                        (user-directory 'downloads))
+        (insert "foo"))
+      (should (string-match "new-name"
+                            (user-file 'downloads "new-name" old-name))))))
+
+(ert-deftest user-directory-tests-user-file/creates-dir-if-missing ()
+  ;; Already tested for `user-directory' but let's make sure.
+  (with-user-directory-test
+    (ert-with-temp-directory dir
+      (let ((user-directory-alist `((downloads . ,dir))))
+        (delete-directory dir)
+        (user-file 'downloads "foo/bar")
+        (should (file-exists-p dir))
+        ;; Sanity checks.
+        (should-not (file-exists-p (expand-file-name "foo" dir)))
+        (should-not (file-exists-p (expand-file-name "foo/bar")))))))
+
+
+;;;; Internal
+
+(ert-deftest user-directory--find-or-create-dir ()
+  (ert-with-temp-directory dir1
+    (should (equal dir1 (car (user-directory--find-or-create-dir
+                              (list dir1)))))))
+
+(ert-deftest user-directory--find-or-create-dir/creates-directory ()
+  (ert-with-temp-directory dir
+    (let ((new-dir (expand-file-name "foo" dir)))
+      (user-directory--find-or-create-dir (list new-dir))
+      (should (file-directory-p new-dir)))))
+
+(ert-deftest user-directory-tests--find-or-create-dir/skips-inacessible ()
+  (ert-with-temp-directory dir1
+    (ert-with-temp-directory dir2
+      (chmod dir1 #x000)
+      (should (equal dir2 (car (user-directory--find-or-create-dir
+                                (list dir1 dir2)))))
+      (should (equal dir2 (car (user-directory--find-or-create-dir
+                                (list dir2 dir1))))))))
+
+;;; user-directory-tests.el ends here



reply via email to

[Prev in Thread] Current Thread [Next in Thread]