RAD Forms support file uploads with download/preview of previously-uploaded files. Files are tracked by SHA256 fingerprints, uploaded to temporary storage during form editing, then moved to permanent storage (S3, disk, etc.) on form save. RAD uses Fulcro's file upload mechanisms for transfer and provides middleware for both client and server.
From DevelopersGuide.adoc:1866-1880:
Components:
From DevelopersGuide.adoc:1872-1874:
"EQL resolvers send transit, so it is not possible to query for the file content via a Pathom resolver. Instead you must supply a resolver that can, given the current parsing context, resolve the URL of the file's content for download by the UI."
From DevelopersGuide.adoc:1875-1879:
"File transfer support leverages Fulcro's normal file upload mechanisms for upload and the normal HTTP GET mechanisms for download. The file is sent as a separate upload mutation during form interaction, and upload progress blocks exiting the form until the upload is complete... The file itself is stored on the server as a temporary file until such time as you save the form itself (though you can also configure the form to auto-save when upload is complete)."
From DevelopersGuide.adoc:1881-1905:
From DevelopersGuide.adoc:1883-1887:
"RAD's built-in support for BLOBs requires that you define a place in one of your database stores to keep a fingerprint for the file. RAD uses SHA256 to generate such a fingerprint for files (much like
git). The fingerprint is treated as the key to the binary data in the store where you place the bytes of the file. This allows you to do things like duplicate detection, and can help in situations where many users might upload the same content (your regular database would track who has access to what files, but they'd be deduped)."
From DevelopersGuide.adoc:1893-1904:
From DevelopersGuide.adoc:1935-1941:
"Since RAD controls the rendering of the file in forms it needs to know how to group together attributes of a file so that it knows which is the filename, which is the URL, etc. RAD does this by keyword "narrowing", our term for the process of using the current attribute's full name as a namespace (by replacing
/with.) and adding a new name.Thus, if you define a blob attribute
:file/shathen the filename attribute will be assumed to be:file.sha/filenameby the auto-generated UI in RAD."
Pattern:
:file/sha:file.sha/filename, :file.sha/url, :file.sha/progress, :file.sha/statusFrom DevelopersGuide.adoc:1942-1975:
(ns com.example.model.file
(:require
[com.fulcrologic.rad.attributes :refer [defattr]]
[com.fulcrologic.rad.attributes-options :as ao]
[com.fulcrologic.rad.blob :as blob]))
(defattr id :file/id :uuid
{ao/identity? true
ao/schema :production})
;; :files is the name of the BLOB store, :remote is the Fulcro remote for uploads
(blob/defblobattr sha :file/sha :files :remote
{ao/identities #{:file/id}
ao/schema :production})
(defattr filename :file.sha/filename :string
{ao/schema :production
ao/identities #{:file/id}})
(defattr uploaded-on :file/uploaded-on :instant
{ao/schema :production
ao/identities #{:file/id}})
(def attributes [id sha filename uploaded-on])
From DevelopersGuide.adoc:1977-1978:
"The
defblobattrrequires you supply a keyword for the attribute, the name of the permanent store for the content (:filesin this example), and the name of the Fulcro client remote (:remotein this example) that can transmit the file bytes."
Signature: (blob/defblobattr sym keyword store-name remote-name options)
From DevelopersGuide.adoc:1979-1993:
(ns com.example.client
(:require
[com.fulcrologic.fulcro.application :as app]
[com.fulcrologic.fulcro.networking.http-remote :as http]
[com.fulcrologic.fulcro.networking.file-upload :as file-upload]))
(def request-middleware
(->
(http/wrap-fulcro-request)
(file-upload/wrap-file-upload)))
(defonce app
(app/fulcro-app
{:remotes {:remote (http/fulcro-http-remote
{:url "/api"
:request-middleware request-middleware})}}))
From DevelopersGuide.adoc:1981-1982:
"You must configure an HTTP remote on the client that includes the Fulcro file upload middleware. This is covered in the Fulcro Developer's guide..."
From DevelopersGuide.adoc:1995-2080:
From DevelopersGuide.adoc:1999-2005:
"First, you need to define a temporary and permanent store. RAD requires a store to implement the
com.fulcrologic.rad.blob-storage/Storageprotocol. The temporary store can just use the pre-supplied transient store, which uses (and tries to garbage collect) temporary disk files on your server's disk. RAD's transient store requires connection stickiness so that the eventual form save will go to the save server as the temporary store."
(ns com.example.components.blob-store
(:require
[com.fulcrologic.rad.blob-storage :as storage]
[mount.core :refer [defstate]]))
(defstate temporary-blob-store
:start
(storage/transient-blob-store "" 1))
(defstate file-blob-store
:start
(storage/transient-blob-store "/files" 10000))
storage/transient-blob-store Parameters:
url-prefix - URL prefix for file downloads (e.g., "/files")max-age-seconds - How long to keep orphaned files (1 sec for temp, 10000 for permanent)From DevelopersGuide.adoc:2006-2052:
(ns com.example.components.ring-middleware
(:require
[com.example.components.blob-store :as bs]
[com.example.components.config :as config]
[com.fulcrologic.fulcro.networking.file-upload :as file-upload]
[com.fulcrologic.fulcro.server.api-middleware :as server]
[com.fulcrologic.rad.blob :as blob]
[mount.core :refer [defstate]]
[ring.middleware.defaults :refer [wrap-defaults]]
[ring.util.response :as resp]
[taoensso.timbre :as log]))
(defstate middleware
:start
(let [defaults-config (:ring.middleware/defaults-config config/config)]
(-> not-found-handler
(wrap-api "/api")
;; Fulcro support for integrated file uploads
(file-upload/wrap-mutation-file-uploads {})
;; RAD integration for *serving* files FROM RAD blob store (at /files URI)
(blob/wrap-blob-service "/files" bs/file-blob-store)
(server/wrap-transit-params {})
(server/wrap-transit-response {})
(wrap-defaults defaults-config))))
From DevelopersGuide.adoc:2008-2009:
"There are two parts to the Ring middleware, and one is optional and is only necessary if you plan to serve the BLOB URLs from your server."
Required:
file-upload/wrap-mutation-file-uploads - Handles incoming file uploadsOptional:
blob/wrap-blob-service - Serves file content at specified URI (e.g., /files/<SHA>)From DevelopersGuide.adoc:2055-2080:
(ns com.example.components.parser
(:require
[com.example.components.blob-store :as bs]
[com.example.model.attributes :refer [all-attributes]]
[com.fulcrologic.rad.blob :as blob]
[com.fulcrologic.rad.pathom :as pathom]
[mount.core :refer [defstate]]))
(defstate parser
:start
(pathom/new-parser config
[; ... other plugins ...
;; Enables binary object upload integration with RAD
(blob/pathom-plugin bs/temporary-blob-store {:files bs/file-blob-store})
; ... other plugins ...
]
[resolvers
; ... other resolvers ...
(blob/resolvers all-attributes)]))
From DevelopersGuide.adoc:2082-2084:
"The blob plugin mainly puts the temporary store and permanent store(s) into the parsing env so that they are available when built-in blob-related reads/mutations are called. The BLOB resolvers use the keyword narrowing of your SHA attribute and the
envto provide values that can be derived from the SHA and the store (i.e.:file.sha/url)."
From DevelopersGuide.adoc:2085-2090:
Define one SHA attribute on the entity:
(blob/defblobattr avatar :user/avatar :files :remote
{ao/identities #{:user/id}})
(defattr avatar-filename :user.avatar/filename :string
{ao/identities #{:user/id}})
Define multiple SHA attributes:
(blob/defblobattr thumbnail :product/thumbnail :files :remote
{ao/identities #{:product/id}})
(blob/defblobattr main-image :product/main-image :files :remote
{ao/identities #{:product/id}})
From DevelopersGuide.adoc:2087-2090:
"You can also support general to-many support for files simply by creating a
refattribute that refers to a entity/row/document that has a file SHA on it."
;; Attachment entity
(defattr id :attachment/id :uuid
{ao/identity? true})
(blob/defblobattr content :attachment/content :files :remote
{ao/identities #{:attachment/id}})
(defattr filename :attachment.content/filename :string
{ao/identities #{:attachment/id}})
;; Invoice references many attachments
(defattr attachments :invoice/attachments :ref
{ao/target :attachment/id
ao/cardinality :many
ao/identities #{:invoice/id}})
From DevelopersGuide.adoc:2091-2109:
From DevelopersGuide.adoc:2097-2109:
(ns com.example.ui.file-form
(:require
[com.fulcrologic.fulcro.components :as comp]
[com.fulcrologic.fulcro.dom :as dom]
[com.fulcrologic.rad.blob :as blob]
[com.fulcrologic.rad.form :as form]))
(form/defsc-form FileForm [this props]
{fo/id file/id
fo/attributes [file/sha file/uploaded-on]
fo/subforms {}}
(dom/div
(dom/input {:type "file"
:onChange (fn [evt]
(when-let [js-file (-> evt .-target .-files (aget 0))]
(blob/upload-file! this file/sha js-file
{:file-ident (comp/get-ident this)})))})))
From DevelopersGuide.adoc:2103-2109:
"Assuming
thisrepresents the UI instance that has the file upload field, the call to start an upload is:
(blob/upload-file! this blob-attribute js-file {:file-ident (comp/get-ident this)})If your
blob-attributehad the keyword:file/shathen you'd see a:file.sha/progressand:file.sha/statusappear on that entity and update as the file upload progresses. Saving the form should then automatically move the file content (named by SHA) from temporary to permanent storage."
Auto-Generated Attributes:
:file.sha/progress - Upload percentage (0-100):file.sha/status - Upload status (:uploading, :complete, :failed)From DevelopersGuide.adoc:2110-2120:
From DevelopersGuide.adoc:2112-2120:
"The
Storageprotocol defines ablob-urlmethod. This method is under the control of the implementation, of course, and may do nothing more than return the SHA you hand it. You are really responsible for hooking RAD up to a binary store that works for your deployment. The built-in support assumes that you'll serve the file content through your server for access control. The provided middleware simply asks the Storage protocol for a stream of the file's bytes, and serves them at a URI on your server.Thus, you might configure your permanent blob store to return the URL
/files/<SHA>, and then configure your Ring middleware to provide the correct file when asked for/files/<SHA>. This is what the middleware configuration shown earlier will do."
Flow:
:file.sha/url via blob/resolvers/files/<SHA>blob/wrap-blob-service middleware intercepts(ns com.example.model.user
(:require
[com.fulcrologic.rad.blob :as blob]
[com.fulcrologic.rad.attributes :refer [defattr]]
[com.fulcrologic.rad.attributes-options :as ao]))
(defattr id :user/id :uuid
{ao/identity? true
ao/schema :production})
(blob/defblobattr avatar :user/avatar :files :remote
{ao/identities #{:user/id}
ao/schema :production})
(defattr avatar-filename :user.avatar/filename :string
{ao/identities #{:user/id}
ao/schema :production})
;; Document entity with file
(defattr id :document/id :uuid
{ao/identity? true})
(blob/defblobattr content :document/content :files :remote
{ao/identities #{:document/id}})
(defattr content-filename :document.content/filename :string
{ao/identities #{:document/id}})
(defattr uploaded-by :document/uploaded-by :ref
{ao/target :user/id
ao/identities #{:document/id}})
;; Project has many documents
(defattr documents :project/documents :ref
{ao/target :document/id
ao/cardinality :many
ao/identities #{:project/id}})
(ns com.example.storage.s3
(:require
[com.fulcrologic.rad.blob-storage :as storage]))
(defrecord S3BlobStore [bucket prefix]
storage/Storage
(save-blob [this sha input-stream]
; Upload to S3 bucket
(s3/put-object bucket (str prefix sha) input-stream))
(blob-url [this sha]
; Return CloudFront URL or signed S3 URL
(str "https://cdn.example.com/" sha))
(blob-stream [this sha]
; Download from S3
(:input-stream (s3/get-object bucket (str prefix sha))))
(delete-blob [this sha]
(s3/delete-object bucket (str prefix sha))))
(defn s3-blob-store [bucket prefix]
(->S3BlobStore bucket prefix))
(form/defsc-form DocumentForm [this {:document/keys [id] :as props}]
{fo/id document/id
fo/attributes [document/content document/uploaded-by]}
(let [url (get props :document.content/url)
filename (get props :document.content/filename)]
(dom/div
(when url
(dom/div
(dom/h3 "Current File")
(dom/a {:href url :target "_blank"} filename)))
(dom/input {:type "file"
:onChange (fn [evt]
(when-let [js-file (-> evt .-target .-files (aget 0))]
(blob/upload-file! this document/content js-file
{:file-ident [:document/id id]})))}))))
From DevelopersGuide.adoc:2003-2005:
"RAD's transient store requires connection stickiness so that the eventual form save will go to the save server as the temporary store. If that is not possible in your deployment then you may wish to use your permanent store as the temporary store and just plan on cleaning up stray files at some future time."
Files are identified by SHA256. This enables:
From DevelopersGuide.adoc:1936-1941:
Given :file/sha, RAD automatically looks for:
:file.sha/filename:file.sha/url:file.sha/progress:file.sha/statusConvention: Follow this pattern for greenfield projects. Use rewrite middleware if legacy schema differs.
From DevelopersGuide.adoc:1876-1877:
"Upload progress blocks exiting the form until the upload is complete (the form field itself for the upload relies on correctly-installed validation for this to function)."
From DevelopersGuide.adoc:1872-1874:
You cannot query for file bytes through Pathom. Instead, query for the URL and use HTTP GET to download.
(-> handler
(wrap-api "/api")
(file-upload/wrap-mutation-file-uploads {}) ; Must be before wrap-api
(blob/wrap-blob-service "/files" store) ; Can be anywhere
(server/wrap-transit-params {})
(server/wrap-transit-response {}))
Implement com.fulcrologic.rad.blob-storage/Storage for custom storage:
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |