|
217 | 217 | "Recursively copy `src` into `dst`, creating `dst` if needed. |
218 | 218 | Used to seed staging subdirectories with the previous export so an |
219 | 219 | incremental run does not lose files it chooses not to rewrite (see |
220 | | - the call site). Relies on `file-seq`'s pre-order traversal so each |
| 220 | + the call site). Copies file attributes so a seeded file the run |
| 221 | + keeps retains its mtime -- the web server derives ETag/Last-Modified |
| 222 | + from it, and conditional GETs would otherwise re-download unchanged |
| 223 | + files. Relies on `file-seq`'s pre-order traversal so each |
221 | 224 | directory is created before its children are copied." |
222 | 225 | [^java.io.File src ^java.io.File dst] |
223 | 226 | (when (.exists src) |
224 | 227 | (let [src-path (.toPath src) |
225 | 228 | dst-path (.toPath dst) |
226 | 229 | opts (into-array java.nio.file.CopyOption |
227 | | - [java.nio.file.StandardCopyOption/REPLACE_EXISTING])] |
| 230 | + [java.nio.file.StandardCopyOption/REPLACE_EXISTING |
| 231 | + java.nio.file.StandardCopyOption/COPY_ATTRIBUTES])] |
228 | 232 | (doseq [^java.io.File f (file-seq src) |
229 | 233 | :let [target (.resolve dst-path (.relativize src-path (.toPath f)))]] |
230 | 234 | (if (.isDirectory f) |
|
834 | 838 | ;; Per-source export functions |
835 | 839 | ;; --------------------------------------------------------------------------- |
836 | 840 |
|
| 841 | +(defn- json-unchanged? |
| 842 | + "True when `file` already holds the JSON we are about to write. |
| 843 | + Incremental staging is seeded from the previous export with mtimes |
| 844 | + preserved, so leaving an identical file untouched keeps its mtime |
| 845 | + and thus the ETag/Last-Modified the web server derives from it. |
| 846 | + When `envelope` carries the per-run :generated stamp, compare the |
| 847 | + parsed data without it: skipped runs already leave the stamp stale, |
| 848 | + so it effectively tracks the last time the data changed." |
| 849 | + [file json-str envelope] |
| 850 | + (let [f (io/file file)] |
| 851 | + (and (.exists f) |
| 852 | + (let [prev (slurp f)] |
| 853 | + (if (contains? envelope :generated) |
| 854 | + (try (= (dissoc (json/parse-string prev true) :generated) |
| 855 | + (dissoc (json/parse-string json-str true) :generated)) |
| 856 | + (catch Exception _ false)) |
| 857 | + (= prev json-str)))))) |
| 858 | + |
837 | 859 | (defn dump-json! |
838 | | - "Dump reports as JSON for a single source." |
| 860 | + "Dump reports as JSON for a single source. |
| 861 | + Leaves a file whose content did not change untouched (see |
| 862 | + `json-unchanged?`)." |
839 | 863 | ([reports out-dir source-name source-map maintainers-map] |
840 | 864 | (dump-json! reports out-dir source-name source-map maintainers-map "all.json" nil)) |
841 | 865 | ([reports out-dir source-name source-map maintainers-map basename] |
|
848 | 872 | :reports data} |
849 | 873 | (seq meta) (merge meta) |
850 | 874 | (seq extra-meta) (merge extra-meta)) |
851 | | - filename (str out-dir "/" basename)] |
852 | | - (spit filename (json/generate-string envelope {:pretty true})) |
853 | | - (when (seq data) |
854 | | - (log/info "Wrote" (count data) "reports to" filename))))) |
| 875 | + filename (str out-dir "/" basename) |
| 876 | + json-str (json/generate-string envelope {:pretty true})] |
| 877 | + (if (json-unchanged? filename json-str envelope) |
| 878 | + (log/info "Unchanged" filename) |
| 879 | + (do (spit filename json-str) |
| 880 | + (when (seq data) |
| 881 | + (log/info "Wrote" (count data) "reports to" filename))))))) |
855 | 882 |
|
856 | 883 | (def ^:private rfc822-formatter |
857 | 884 | ;; DateTimeFormatter is immutable and thread-safe. Pattern matches |
|
0 commit comments