diff --git a/README.md b/README.md index 64dbd5c..642fec7 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,11 @@ io.github.latacora/sqlite-cache {:git/url "https://github.com/latacora/sqlite-ca :read {"some-tag" read-handler}}}) ``` +Filesystem `:dbname` paths may include missing parent directories; `cache` +creates them before opening SQLite. New filesystem databases on POSIX systems +are created with mode `0600`; existing files keep their permissions. SQLite +URI paths (`file:...`) and in-memory databases are passed through unchanged. + ### Supported types The cache uses [transit-canon](https://github.com/latacora/transit-canon) for serialization, so out of the box it supports: diff --git a/deps.edn b/deps.edn index aa51991..e525325 100644 --- a/deps.edn +++ b/deps.edn @@ -1,6 +1,7 @@ {:paths ["src" "resources"] :deps {org.clojure/clojure {:mvn/version "1.12.4"} + babashka/fs {:mvn/version "0.5.22"} ;; Database com.github.seancorfield/next.jdbc {:mvn/version "1.3.1070"} diff --git a/src/com/latacora/sqlite_cache/core.clj b/src/com/latacora/sqlite_cache/core.clj index 3769f18..d1300fd 100644 --- a/src/com/latacora/sqlite_cache/core.clj +++ b/src/com/latacora/sqlite_cache/core.clj @@ -14,6 +14,8 @@ All cache analysis and cleanup (actually deleting expired records and VACUUMing afterwards) is done manually (see `maintain!`)." (:require + [babashka.fs :as fs] + [clojure.string :as str] [honey.sql.helpers :as h] [next.jdbc :as jdbc] [com.latacora.sqlite-cache.serialization :as ser] @@ -21,6 +23,7 @@ [com.latacora.sqlite-cache.ddl :as ddl] [com.latacora.sqlite-cache.maintenance :as maint]) (:import + (java.nio.file FileAlreadyExistsException) (java.util.concurrent LinkedBlockingQueue TimeUnit))) ;; This currently does not use clojure.core.cache, but it probably could stand @@ -214,11 +217,25 @@ :ttl default-ttl :max-age default-max-age}) +(defn- prepare-db-file! + [{:keys [dbname]}] + (when (and dbname (not= dbname ":memory:") + (not (str/starts-with? dbname "file:"))) + (let [target (fs/absolutize dbname)] + (fs/create-dirs (fs/parent target)) + (try + (fs/create-file target {:posix-file-permissions "rw-------"}) + (catch FileAlreadyExistsException _ nil) + ;; Let SQLite use platform defaults when POSIX permissions are unavailable. + (catch UnsupportedOperationException _ nil))))) + (defn cache "Builds a cache with given opts. This will ensure the cache is ready to use (has the appropriate schema, - indexes, et cetera), and returns a function with the same signature as the + indexes, et cetera), creates missing parent directories for filesystem + `:dbname` paths, creates new POSIX database files with owner-only access, + and returns a function with the same signature as the function being cached. The returned function has metadata containing the full cache configuration, @@ -245,6 +262,7 @@ are required for cache-key correctness." [opts] (let [{:keys [db] :as opts} (merge default-opts opts) + _ (prepare-db-file! db) read-conn (jdbc/get-connection db) write-conn (jdbc/get-connection db) write-queue (make-write-queue! write-conn) diff --git a/test/com/latacora/sqlite_cache/core_test.clj b/test/com/latacora/sqlite_cache/core_test.clj index ced767e..406bd2b 100644 --- a/test/com/latacora/sqlite_cache/core_test.clj +++ b/test/com/latacora/sqlite_cache/core_test.clj @@ -1,5 +1,6 @@ (ns com.latacora.sqlite-cache.core-test (:require + [babashka.fs :as fs] [cognitect.transit :as transit] [com.latacora.sqlite-cache.core :as c] [com.latacora.sqlite-cache.maintenance :as maint] @@ -690,3 +691,20 @@ ;; Second call should succeed with the same arguments (t/is (= 10 (cached-fn 5))))))) + +(t/deftest creates-missing-cache-parent-directories + (let [root (fs/create-temp-dir {:prefix "sqlite-cache-parents-"}) + path (fs/path root "nested" "cache.db")] + (try + (t/is (not (fs/exists? (fs/parent path)))) + (let [cached (c/cache {:db {:dbtype "sqlite" :dbname (str path)} + :func inc :func-name "test/parents"})] + (t/is (fs/regular-file? path)) + (when-let [permissions (try + (fs/posix-file-permissions path) + (catch UnsupportedOperationException _ nil))] + (t/is (= "rw-------" (fs/posix->str permissions)))) + (t/is (= 2 (cached 1))) + (tu/assert-n-entries! cached 1)) + (finally + (fs/delete-tree root)))))