2020-12-30 14:38:00 +01:00
|
|
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
;;
|
2021-04-10 09:43:04 +02:00
|
|
|
;; Copyright (c) UXBOX Labs SL
|
2020-12-30 14:38:00 +01:00
|
|
|
|
|
|
|
(ns app.storage
|
2021-06-14 11:50:26 +02:00
|
|
|
"Objects storage abstraction layer."
|
2020-12-30 14:38:00 +01:00
|
|
|
(:require
|
|
|
|
[app.common.data :as d]
|
2022-02-24 23:36:53 +01:00
|
|
|
[app.common.data.macros :as dm]
|
2020-12-30 14:38:00 +01:00
|
|
|
[app.common.exceptions :as ex]
|
2021-09-29 16:39:25 +02:00
|
|
|
[app.common.logging :as l]
|
2020-12-30 14:38:00 +01:00
|
|
|
[app.common.spec :as us]
|
|
|
|
[app.common.uuid :as uuid]
|
|
|
|
[app.db :as db]
|
2021-01-04 18:41:05 +01:00
|
|
|
[app.storage.db :as sdb]
|
2020-12-30 14:38:00 +01:00
|
|
|
[app.storage.fs :as sfs]
|
|
|
|
[app.storage.impl :as impl]
|
|
|
|
[app.storage.s3 :as ss3]
|
|
|
|
[app.util.time :as dt]
|
2022-02-28 17:15:58 +01:00
|
|
|
[app.worker :as wrk]
|
2020-12-30 14:38:00 +01:00
|
|
|
[clojure.spec.alpha :as s]
|
2021-01-25 16:14:54 +01:00
|
|
|
[datoteka.core :as fs]
|
2022-02-28 17:15:58 +01:00
|
|
|
[integrant.core :as ig]
|
|
|
|
[promesa.core :as p]
|
|
|
|
[promesa.exec :as px]))
|
2020-12-30 14:38:00 +01:00
|
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;; Storage Module State
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
2021-01-25 15:22:39 +01:00
|
|
|
(s/def ::s3 ::ss3/backend)
|
|
|
|
(s/def ::fs ::sfs/backend)
|
|
|
|
(s/def ::db ::sdb/backend)
|
|
|
|
|
2020-12-30 14:38:00 +01:00
|
|
|
(s/def ::backends
|
2021-06-14 11:50:26 +02:00
|
|
|
(s/map-of ::us/keyword
|
|
|
|
(s/nilable
|
|
|
|
(s/or :s3 ::ss3/backend
|
|
|
|
:fs ::sfs/backend
|
|
|
|
:db ::sdb/backend))))
|
2020-12-30 14:38:00 +01:00
|
|
|
|
|
|
|
(defmethod ig/pre-init-spec ::storage [_]
|
2022-02-28 17:15:58 +01:00
|
|
|
(s/keys :req-un [::db/pool ::wrk/executor ::backends]))
|
2020-12-30 14:38:00 +01:00
|
|
|
|
|
|
|
(defmethod ig/prep-key ::storage
|
|
|
|
[_ {:keys [backends] :as cfg}]
|
2021-01-04 18:41:05 +01:00
|
|
|
(-> (d/without-nils cfg)
|
|
|
|
(assoc :backends (d/without-nils backends))))
|
2020-12-30 14:38:00 +01:00
|
|
|
|
|
|
|
(defmethod ig/init-key ::storage
|
2021-06-14 11:50:26 +02:00
|
|
|
[_ {:keys [backends] :as cfg}]
|
|
|
|
(-> (d/without-nils cfg)
|
|
|
|
(assoc :backends (d/without-nils backends))))
|
2020-12-30 14:38:00 +01:00
|
|
|
|
2021-01-25 16:14:54 +01:00
|
|
|
(s/def ::storage
|
2022-02-10 19:50:40 +01:00
|
|
|
(s/keys :req-un [::backends ::db/pool]))
|
2021-01-25 16:14:54 +01:00
|
|
|
|
2020-12-30 14:38:00 +01:00
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;; Database Objects
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
2022-02-10 19:50:40 +01:00
|
|
|
(defrecord StorageObject [id size created-at expired-at touched-at backend])
|
2020-12-30 14:38:00 +01:00
|
|
|
|
2021-01-31 11:50:21 +01:00
|
|
|
(defn storage-object?
|
|
|
|
[v]
|
|
|
|
(instance? StorageObject v))
|
|
|
|
|
2022-02-10 19:50:40 +01:00
|
|
|
(s/def ::storage-object storage-object?)
|
|
|
|
(s/def ::storage-content impl/content?)
|
|
|
|
|
2022-02-28 17:15:58 +01:00
|
|
|
(defn get-metadata
|
|
|
|
[params]
|
|
|
|
(into {}
|
|
|
|
(remove (fn [[k _]] (qualified-keyword? k)))
|
|
|
|
params))
|
|
|
|
|
|
|
|
(defn- get-database-object-by-hash
|
|
|
|
[conn backend bucket hash]
|
|
|
|
(let [sql (str "select * from storage_object "
|
|
|
|
" where (metadata->>'~:hash') = ? "
|
|
|
|
" and (metadata->>'~:bucket') = ? "
|
|
|
|
" and backend = ?"
|
|
|
|
" and deleted_at is null"
|
|
|
|
" limit 1")]
|
|
|
|
(db/exec-one! conn [sql hash bucket (name backend)])))
|
2021-01-25 15:22:39 +01:00
|
|
|
|
2020-12-30 14:38:00 +01:00
|
|
|
(defn- create-database-object
|
2022-02-28 17:15:58 +01:00
|
|
|
[{:keys [conn backend executor]} {:keys [::content ::expired-at ::touched-at] :as params}]
|
2022-02-10 19:50:40 +01:00
|
|
|
(us/assert ::storage-content content)
|
2022-02-28 17:15:58 +01:00
|
|
|
(px/with-dispatch executor
|
|
|
|
(let [id (uuid/random)
|
|
|
|
|
|
|
|
mdata (cond-> (get-metadata params)
|
|
|
|
(satisfies? impl/IContentHash content)
|
|
|
|
(assoc :hash (impl/get-hash content)))
|
|
|
|
|
|
|
|
;; NOTE: for now we don't reuse the deleted objects, but in
|
|
|
|
;; futute we can consider reusing deleted objects if we
|
|
|
|
;; found a duplicated one and is marked for deletion but
|
|
|
|
;; still not deleted.
|
|
|
|
result (when (and (::deduplicate? params)
|
|
|
|
(:hash mdata)
|
|
|
|
(:bucket mdata))
|
|
|
|
(get-database-object-by-hash conn backend (:bucket mdata) (:hash mdata)))
|
|
|
|
|
|
|
|
result (or result
|
|
|
|
(db/insert! conn :storage-object
|
|
|
|
{:id id
|
|
|
|
:size (count content)
|
|
|
|
:backend (name backend)
|
|
|
|
:metadata (db/tjson mdata)
|
|
|
|
:deleted-at expired-at
|
|
|
|
:touched-at touched-at}))]
|
|
|
|
|
|
|
|
(StorageObject. (:id result)
|
|
|
|
(:size result)
|
|
|
|
(:created-at result)
|
|
|
|
(:deleted-at result)
|
|
|
|
(:touched-at result)
|
|
|
|
backend
|
|
|
|
mdata
|
|
|
|
nil))))
|
2020-12-30 14:38:00 +01:00
|
|
|
|
|
|
|
(def ^:private sql:retrieve-storage-object
|
2021-01-25 15:22:39 +01:00
|
|
|
"select * from storage_object where id = ? and (deleted_at is null or deleted_at > now())")
|
2020-12-30 14:38:00 +01:00
|
|
|
|
2021-01-19 15:04:28 +01:00
|
|
|
(defn row->storage-object [res]
|
2022-02-10 19:50:40 +01:00
|
|
|
(let [mdata (or (some-> (:metadata res) (db/decode-transit-pgobject)) {})]
|
2021-01-19 15:04:28 +01:00
|
|
|
(StorageObject. (:id res)
|
|
|
|
(:size res)
|
|
|
|
(:created-at res)
|
2021-01-25 15:22:39 +01:00
|
|
|
(:deleted-at res)
|
2022-02-10 19:50:40 +01:00
|
|
|
(:touched-at res)
|
2021-01-19 15:04:28 +01:00
|
|
|
(keyword (:backend res))
|
|
|
|
mdata
|
|
|
|
nil)))
|
|
|
|
|
2020-12-30 14:38:00 +01:00
|
|
|
(defn- retrieve-database-object
|
2021-01-04 18:41:05 +01:00
|
|
|
[{:keys [conn] :as storage} id]
|
2020-12-30 14:38:00 +01:00
|
|
|
(when-let [res (db/exec-one! conn [sql:retrieve-storage-object id])]
|
2021-01-19 15:04:28 +01:00
|
|
|
(row->storage-object res)))
|
2020-12-30 14:38:00 +01:00
|
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;; API
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
2021-01-30 11:28:11 +01:00
|
|
|
(defn object->relative-path
|
|
|
|
[{:keys [id] :as obj}]
|
|
|
|
(impl/id->path id))
|
|
|
|
|
|
|
|
(defn file-url->path
|
|
|
|
[url]
|
|
|
|
(fs/path (java.net.URI. (str url))))
|
|
|
|
|
2022-02-28 17:15:58 +01:00
|
|
|
(dm/export impl/content)
|
|
|
|
(dm/export impl/wrap-with-hash)
|
2020-12-30 14:38:00 +01:00
|
|
|
|
|
|
|
(defn get-object
|
2021-01-04 18:41:05 +01:00
|
|
|
[{:keys [conn pool] :as storage} id]
|
2021-01-25 16:14:54 +01:00
|
|
|
(us/assert ::storage storage)
|
2022-02-28 17:15:58 +01:00
|
|
|
(p/do
|
|
|
|
(-> (assoc storage :conn (or conn pool))
|
|
|
|
(retrieve-database-object id))))
|
2020-12-30 14:38:00 +01:00
|
|
|
|
2022-02-28 17:15:58 +01:00
|
|
|
(defn put-object!
|
2021-01-30 11:28:11 +01:00
|
|
|
"Creates a new object with the provided content."
|
2022-02-28 17:15:58 +01:00
|
|
|
[{:keys [pool conn backend] :as storage} {:keys [::content] :as params}]
|
2021-01-25 16:14:54 +01:00
|
|
|
(us/assert ::storage storage)
|
2022-02-10 19:50:40 +01:00
|
|
|
(us/assert ::storage-content content)
|
|
|
|
(us/assert ::us/keyword backend)
|
2022-02-28 17:15:58 +01:00
|
|
|
(p/let [storage (assoc storage :conn (or conn pool))
|
|
|
|
object (create-database-object storage params)]
|
2021-01-04 18:41:05 +01:00
|
|
|
|
|
|
|
;; Store the data finally on the underlying storage subsystem.
|
2021-06-14 11:50:26 +02:00
|
|
|
(-> (impl/resolve-backend storage backend)
|
2020-12-30 14:38:00 +01:00
|
|
|
(impl/put-object object content))
|
2021-01-04 18:41:05 +01:00
|
|
|
|
2020-12-30 14:38:00 +01:00
|
|
|
object))
|
|
|
|
|
2022-02-28 17:15:58 +01:00
|
|
|
(defn touch-object!
|
|
|
|
"Mark object as touched."
|
|
|
|
[{:keys [pool conn] :as storage} object-or-id]
|
|
|
|
(p/do
|
|
|
|
(let [id (if (storage-object? object-or-id) (:id object-or-id) object-or-id)
|
|
|
|
res (db/update! (or conn pool) :storage-object
|
|
|
|
{:touched-at (dt/now)}
|
|
|
|
{:id id}
|
|
|
|
{:return-keys false})]
|
|
|
|
(pos? (:next.jdbc/update-count res)))))
|
2021-01-04 18:41:05 +01:00
|
|
|
|
2020-12-30 14:38:00 +01:00
|
|
|
(defn get-object-data
|
2021-06-14 11:50:26 +02:00
|
|
|
"Return an input stream instance of the object content."
|
2020-12-30 14:38:00 +01:00
|
|
|
[{:keys [pool conn] :as storage} object]
|
2021-01-25 16:14:54 +01:00
|
|
|
(us/assert ::storage storage)
|
2022-02-28 17:15:58 +01:00
|
|
|
(p/do
|
|
|
|
(when (or (nil? (:expired-at object))
|
|
|
|
(dt/is-after? (:expired-at object) (dt/now)))
|
|
|
|
(-> (assoc storage :conn (or conn pool))
|
|
|
|
(impl/resolve-backend (:backend object))
|
|
|
|
(impl/get-object-data object)))))
|
2020-12-30 14:38:00 +01:00
|
|
|
|
2021-06-14 11:50:26 +02:00
|
|
|
(defn get-object-bytes
|
|
|
|
"Returns a byte array of object content."
|
|
|
|
[{:keys [pool conn] :as storage} object]
|
|
|
|
(us/assert ::storage storage)
|
2022-02-28 17:15:58 +01:00
|
|
|
(p/do
|
|
|
|
(when (or (nil? (:expired-at object))
|
|
|
|
(dt/is-after? (:expired-at object) (dt/now)))
|
|
|
|
(-> (assoc storage :conn (or conn pool))
|
|
|
|
(impl/resolve-backend (:backend object))
|
|
|
|
(impl/get-object-bytes object)))))
|
2021-06-14 11:50:26 +02:00
|
|
|
|
2020-12-30 14:38:00 +01:00
|
|
|
(defn get-object-url
|
|
|
|
([storage object]
|
|
|
|
(get-object-url storage object nil))
|
2021-01-04 18:41:05 +01:00
|
|
|
([{:keys [conn pool] :as storage} object options]
|
2021-01-25 16:14:54 +01:00
|
|
|
(us/assert ::storage storage)
|
2022-02-28 17:15:58 +01:00
|
|
|
(p/do
|
|
|
|
(when (or (nil? (:expired-at object))
|
|
|
|
(dt/is-after? (:expired-at object) (dt/now)))
|
|
|
|
(-> (assoc storage :conn (or conn pool))
|
|
|
|
(impl/resolve-backend (:backend object))
|
|
|
|
(impl/get-object-url object options))))))
|
2020-12-30 14:38:00 +01:00
|
|
|
|
2021-01-30 11:28:11 +01:00
|
|
|
(defn get-object-path
|
|
|
|
"Get the Path to the object. Only works with `:fs` type of
|
|
|
|
storages."
|
2021-01-31 19:25:26 +01:00
|
|
|
[storage object]
|
2022-02-28 17:15:58 +01:00
|
|
|
(p/do
|
|
|
|
(let [backend (impl/resolve-backend storage (:backend object))]
|
|
|
|
(when (not= :fs (:type backend))
|
|
|
|
(ex/raise :type :internal
|
|
|
|
:code :operation-not-allowed
|
|
|
|
:hint "get-object-path only works with fs type backends"))
|
|
|
|
(when (or (nil? (:expired-at object))
|
|
|
|
(dt/is-after? (:expired-at object) (dt/now)))
|
|
|
|
(p/-> (impl/get-object-url backend object nil) file-url->path)))))
|
|
|
|
|
|
|
|
(defn del-object!
|
|
|
|
[{:keys [conn pool] :as storage} object-or-id]
|
2021-01-25 16:14:54 +01:00
|
|
|
(us/assert ::storage storage)
|
2022-02-28 17:15:58 +01:00
|
|
|
(p/do
|
|
|
|
(let [id (if (storage-object? object-or-id) (:id object-or-id) object-or-id)
|
|
|
|
res (db/update! (or conn pool) :storage-object
|
|
|
|
{:deleted-at (dt/now)}
|
|
|
|
{:id id}
|
|
|
|
{:return-keys false})]
|
|
|
|
(pos? (:next.jdbc/update-count res)))))
|
2021-01-25 15:22:39 +01:00
|
|
|
|
2022-02-24 23:36:53 +01:00
|
|
|
(dm/export impl/resolve-backend)
|
2022-02-28 17:15:58 +01:00
|
|
|
(dm/export impl/calculate-hash)
|
2020-12-30 14:38:00 +01:00
|
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
2021-01-29 23:56:11 +01:00
|
|
|
;; Garbage Collection: Permanently delete objects
|
2020-12-30 14:38:00 +01:00
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
|
|
|
;; A task responsible to permanently delete already marked as deleted
|
|
|
|
;; storage files.
|
|
|
|
|
2022-02-10 19:50:40 +01:00
|
|
|
(declare sql:retrieve-deleted-objects-chunk)
|
2020-12-30 14:38:00 +01:00
|
|
|
|
2021-01-25 15:22:39 +01:00
|
|
|
(s/def ::min-age ::dt/duration)
|
|
|
|
|
2021-01-29 23:56:11 +01:00
|
|
|
(defmethod ig/pre-init-spec ::gc-deleted-task [_]
|
2022-02-28 17:15:58 +01:00
|
|
|
(s/keys :req-un [::storage ::db/pool ::min-age ::wrk/executor]))
|
2020-12-30 14:38:00 +01:00
|
|
|
|
2021-01-29 23:56:11 +01:00
|
|
|
(defmethod ig/init-key ::gc-deleted-task
|
2021-01-25 15:22:39 +01:00
|
|
|
[_ {:keys [pool storage min-age] :as cfg}]
|
2022-02-10 19:50:40 +01:00
|
|
|
(letfn [(retrieve-deleted-objects-chunk [conn cursor]
|
2021-01-25 15:22:39 +01:00
|
|
|
(let [min-age (db/interval min-age)
|
2022-02-10 19:50:40 +01:00
|
|
|
rows (db/exec! conn [sql:retrieve-deleted-objects-chunk min-age cursor])]
|
|
|
|
[(some-> rows peek :created-at)
|
|
|
|
(some->> (seq rows) (d/group-by' #(-> % :backend keyword) :id) seq)]))
|
2020-12-30 14:38:00 +01:00
|
|
|
|
2022-02-10 19:50:40 +01:00
|
|
|
(retrieve-deleted-objects [conn]
|
|
|
|
(->> (d/iteration (fn [cursor]
|
|
|
|
(retrieve-deleted-objects-chunk conn cursor))
|
|
|
|
:initk (dt/now)
|
|
|
|
:vf second
|
|
|
|
:kf first)
|
|
|
|
(sequence cat)))
|
|
|
|
|
|
|
|
(delete-in-bulk [conn backend ids]
|
2021-06-14 11:50:26 +02:00
|
|
|
(let [backend (impl/resolve-backend storage backend)
|
2020-12-30 14:38:00 +01:00
|
|
|
backend (assoc backend :conn conn)]
|
2022-02-28 17:15:58 +01:00
|
|
|
@(impl/del-objects-in-bulk backend ids)))]
|
2020-12-30 14:38:00 +01:00
|
|
|
|
2021-01-31 19:25:26 +01:00
|
|
|
(fn [_]
|
2020-12-30 14:38:00 +01:00
|
|
|
(db/with-atomic [conn pool]
|
2022-02-10 19:50:40 +01:00
|
|
|
(loop [total 0
|
|
|
|
groups (retrieve-deleted-objects conn)]
|
|
|
|
(if-let [[backend ids] (first groups)]
|
2021-01-31 17:00:22 +01:00
|
|
|
(do
|
2022-02-10 19:50:40 +01:00
|
|
|
(delete-in-bulk conn backend ids)
|
|
|
|
(recur (+ total (count ids))
|
|
|
|
(rest groups)))
|
2021-01-31 17:00:22 +01:00
|
|
|
(do
|
2022-02-10 19:50:40 +01:00
|
|
|
(l/info :task "gc-deleted" :count total)
|
|
|
|
{:deleted total})))))))
|
2020-12-30 14:38:00 +01:00
|
|
|
|
2022-02-10 19:50:40 +01:00
|
|
|
(def sql:retrieve-deleted-objects-chunk
|
2020-12-30 14:38:00 +01:00
|
|
|
"with items_part as (
|
2021-01-25 15:22:39 +01:00
|
|
|
select s.id
|
|
|
|
from storage_object as s
|
2020-12-30 14:38:00 +01:00
|
|
|
where s.deleted_at is not null
|
2021-01-25 15:22:39 +01:00
|
|
|
and s.deleted_at < (now() - ?::interval)
|
2022-02-10 19:50:40 +01:00
|
|
|
and s.created_at < ?
|
|
|
|
order by s.created_at desc
|
2021-06-04 09:41:42 +02:00
|
|
|
limit 100
|
2020-12-30 14:38:00 +01:00
|
|
|
)
|
|
|
|
delete from storage_object
|
|
|
|
where id in (select id from items_part)
|
|
|
|
returning *;")
|
|
|
|
|
2021-01-29 23:56:11 +01:00
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
2021-11-15 09:53:10 -05:00
|
|
|
;; Garbage Collection: Analyze touched objects
|
2021-01-29 23:56:11 +01:00
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
2022-02-28 17:15:58 +01:00
|
|
|
;; This task is part of the garbage collection of storage objects and
|
|
|
|
;; is responsible on analyzing the touched objects and mark them for
|
|
|
|
;; deletion if corresponds.
|
2021-01-29 23:56:11 +01:00
|
|
|
;;
|
2022-02-28 17:15:58 +01:00
|
|
|
;; For example: when file_media_object is deleted, the depending
|
|
|
|
;; storage_object are marked as touched. This means that some files
|
|
|
|
;; that depend on a concrete storage_object are no longer exists and
|
|
|
|
;; maybe this storage_object is no longer necessary and can be
|
|
|
|
;; eligible for elimination. This task periodically analyzes touched
|
|
|
|
;; objects and mark them as freeze (means that has other references
|
|
|
|
;; and the object is still valid) or deleted (no more references to
|
|
|
|
;; this object so is ready to be deleted).
|
2021-01-29 23:56:11 +01:00
|
|
|
|
2022-02-10 19:50:40 +01:00
|
|
|
(declare sql:retrieve-touched-objects-chunk)
|
|
|
|
(declare sql:retrieve-file-media-object-nrefs)
|
|
|
|
(declare sql:retrieve-team-font-variant-nrefs)
|
2022-02-28 17:15:58 +01:00
|
|
|
(declare sql:retrieve-profile-nrefs)
|
2021-01-29 23:56:11 +01:00
|
|
|
|
|
|
|
(defmethod ig/pre-init-spec ::gc-touched-task [_]
|
|
|
|
(s/keys :req-un [::db/pool]))
|
|
|
|
|
|
|
|
(defmethod ig/init-key ::gc-touched-task
|
|
|
|
[_ {:keys [pool] :as cfg}]
|
2022-02-10 19:50:40 +01:00
|
|
|
(letfn [(has-team-font-variant-nrefs? [conn id]
|
|
|
|
(-> (db/exec-one! conn [sql:retrieve-team-font-variant-nrefs id id id id]) :nrefs pos?))
|
2021-01-31 17:00:22 +01:00
|
|
|
|
2022-02-10 19:50:40 +01:00
|
|
|
(has-file-media-object-nrefs? [conn id]
|
|
|
|
(-> (db/exec-one! conn [sql:retrieve-file-media-object-nrefs id id]) :nrefs pos?))
|
2021-01-29 23:56:11 +01:00
|
|
|
|
2022-02-28 17:15:58 +01:00
|
|
|
(has-profile-nrefs? [conn id]
|
|
|
|
(-> (db/exec-one! conn [sql:retrieve-profile-nrefs id id]) :nrefs pos?))
|
|
|
|
|
2021-01-29 23:56:11 +01:00
|
|
|
(mark-freeze-in-bulk [conn ids]
|
|
|
|
(db/exec-one! conn ["update storage_object set touched_at=null where id = ANY(?)"
|
2022-02-10 19:50:40 +01:00
|
|
|
(db/create-array conn "uuid" ids)]))
|
2021-01-29 23:56:11 +01:00
|
|
|
|
2022-02-10 19:50:40 +01:00
|
|
|
(mark-delete-in-bulk [conn ids]
|
|
|
|
(db/exec-one! conn ["update storage_object set deleted_at=now(), touched_at=null where id = ANY(?)"
|
|
|
|
(db/create-array conn "uuid" ids)]))
|
|
|
|
|
2022-02-28 17:15:58 +01:00
|
|
|
;; NOTE: A getter that retrieves the key witch will be used
|
|
|
|
;; for group ids; previoulsy we have no value, then we
|
|
|
|
;; introduced the `:reference` prop, and then it is renamed
|
|
|
|
;; to `:bucket` and now is string instead. This is
|
|
|
|
;; implemented in this way for backward comaptibilty.
|
|
|
|
|
|
|
|
;; NOTE: we use the "file-media-object" as default value for
|
|
|
|
;; backward compatibility because when we deploy it we can
|
|
|
|
;; have old backend instances running in the same time as
|
|
|
|
;; the new one and we can still have storage-objects created
|
|
|
|
;; without bucket value. And we know that if it does not
|
|
|
|
;; have value, it means :file-media-object.
|
|
|
|
|
|
|
|
(get-bucket [{:keys [metadata]}]
|
|
|
|
(or (some-> metadata :bucket)
|
|
|
|
(some-> metadata :reference d/name)
|
|
|
|
"file-media-object"))
|
|
|
|
|
2022-02-10 19:50:40 +01:00
|
|
|
(retrieve-touched-chunk [conn cursor]
|
|
|
|
(let [rows (->> (db/exec! conn [sql:retrieve-touched-objects-chunk cursor])
|
2022-02-28 17:15:58 +01:00
|
|
|
(mapv #(d/update-when % :metadata db/decode-transit-pgobject)))]
|
2022-02-10 19:50:40 +01:00
|
|
|
(when (seq rows)
|
|
|
|
[(-> rows peek :created-at)
|
2022-02-28 17:15:58 +01:00
|
|
|
(d/group-by' get-bucket :id rows)])))
|
2021-01-04 18:41:05 +01:00
|
|
|
|
2022-02-10 19:50:40 +01:00
|
|
|
(retrieve-touched [conn]
|
|
|
|
(->> (d/iteration (fn [cursor]
|
|
|
|
(retrieve-touched-chunk conn cursor))
|
|
|
|
:initk (dt/now)
|
|
|
|
:vf second
|
|
|
|
:kf first)
|
|
|
|
(sequence cat)))
|
|
|
|
|
|
|
|
(process-objects! [conn pred-fn ids]
|
|
|
|
(loop [to-freeze #{}
|
|
|
|
to-delete #{}
|
|
|
|
ids (seq ids)]
|
|
|
|
(if-let [id (first ids)]
|
|
|
|
(if (pred-fn conn id)
|
|
|
|
(recur (conj to-freeze id) to-delete (rest ids))
|
|
|
|
(recur to-freeze (conj to-delete id) (rest ids)))
|
|
|
|
|
|
|
|
(do
|
|
|
|
(some->> (seq to-freeze) (mark-freeze-in-bulk conn))
|
|
|
|
(some->> (seq to-delete) (mark-delete-in-bulk conn))
|
|
|
|
[(count to-freeze) (count to-delete)]))))
|
|
|
|
]
|
2021-01-04 18:41:05 +01:00
|
|
|
|
2021-01-31 19:25:26 +01:00
|
|
|
(fn [_]
|
2021-01-04 18:41:05 +01:00
|
|
|
(db/with-atomic [conn pool]
|
2022-02-10 19:50:40 +01:00
|
|
|
(loop [to-freeze 0
|
|
|
|
to-delete 0
|
|
|
|
groups (retrieve-touched conn)]
|
2022-02-28 17:15:58 +01:00
|
|
|
(if-let [[bucket ids] (first groups)]
|
|
|
|
(let [[f d] (case bucket
|
|
|
|
"file-media-object" (process-objects! conn has-file-media-object-nrefs? ids)
|
|
|
|
"team-font-variant" (process-objects! conn has-team-font-variant-nrefs? ids)
|
|
|
|
"profile" (process-objects! conn has-profile-nrefs? ids)
|
2022-02-23 09:13:08 +01:00
|
|
|
(ex/raise :type :internal
|
|
|
|
:code :unexpected-unknown-reference
|
2022-02-28 17:15:58 +01:00
|
|
|
:hint (dm/fmt "unknown reference %" bucket)))]
|
2022-02-10 19:50:40 +01:00
|
|
|
(recur (+ to-freeze f)
|
|
|
|
(+ to-delete d)
|
|
|
|
(rest groups)))
|
2021-01-31 17:00:22 +01:00
|
|
|
(do
|
2022-02-10 19:50:40 +01:00
|
|
|
(l/info :task "gc-touched" :to-freeze to-freeze :to-delete to-delete)
|
|
|
|
{:freeze to-freeze :delete to-delete})))))))
|
|
|
|
|
|
|
|
(def sql:retrieve-touched-objects-chunk
|
|
|
|
"select so.* from storage_object as so
|
|
|
|
where so.touched_at is not null
|
|
|
|
and so.created_at < ?
|
|
|
|
order by so.created_at desc
|
|
|
|
limit 500;")
|
|
|
|
|
|
|
|
(def sql:retrieve-file-media-object-nrefs
|
|
|
|
"select ((select count(*) from file_media_object where media_id = ?) +
|
|
|
|
(select count(*) from file_media_object where thumbnail_id = ?)) as nrefs")
|
|
|
|
|
|
|
|
(def sql:retrieve-team-font-variant-nrefs
|
|
|
|
"select ((select count(*) from team_font_variant where woff1_file_id = ?) +
|
|
|
|
(select count(*) from team_font_variant where woff2_file_id = ?) +
|
|
|
|
(select count(*) from team_font_variant where otf_file_id = ?) +
|
|
|
|
(select count(*) from team_font_variant where ttf_file_id = ?)) as nrefs")
|
2022-02-28 17:15:58 +01:00
|
|
|
|
|
|
|
(def sql:retrieve-profile-nrefs
|
|
|
|
"select ((select count(*) from profile where photo_id = ?) +
|
|
|
|
(select count(*) from team where photo_id = ?)) as nrefs")
|