Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2025-01-23 12:53:03 +01:00
commit dae7b7cd74
16 changed files with 192 additions and 208 deletions

View file

@ -1,6 +1,6 @@
# CHANGELOG # CHANGELOG
## 2.5.0 ## 2.5.0 (Unreleased)
### :rocket: Epics and highlights ### :rocket: Epics and highlights
@ -22,6 +22,15 @@
- Fix error when reseting stroke cap - Fix error when reseting stroke cap
- Fix problem with strokes not refreshing in Safari [Taiga #9040](https://tree.taiga.io/project/penpot/issue/9040) - Fix problem with strokes not refreshing in Safari [Taiga #9040](https://tree.taiga.io/project/penpot/issue/9040)
## 2.4.3 (Unreleased)
### :bug: Bugs fixed
- Fix errors from editable select on measures menu [Taiga #9888](https://tree.taiga.io/project/penpot/issue/9888)
- Fix exception on importing some templates from templates slider
## 2.4.2 ## 2.4.2
### :bug: Bugs fixed ### :bug: Bugs fixed
@ -32,6 +41,7 @@
- Fix missing methods reference on API Docs - Fix missing methods reference on API Docs
- Fix memory usage issue on file-gc asynchronous task (related to snapshots feature) - Fix memory usage issue on file-gc asynchronous task (related to snapshots feature)
## 2.4.1 ## 2.4.1
### :bug: Bugs fixed ### :bug: Bugs fixed
@ -39,6 +49,7 @@
- Fix error when importing files with touched components [Taiga #9625](https://tree.taiga.io/project/penpot/issue/9625) - Fix error when importing files with touched components [Taiga #9625](https://tree.taiga.io/project/penpot/issue/9625)
- Fix problem when changing color libraries [Plugins #184](https://github.com/penpot/penpot-plugins/issues/184) - Fix problem when changing color libraries [Plugins #184](https://github.com/penpot/penpot-plugins/issues/184)
## 2.4.0 ## 2.4.0
### :rocket: Epics and highlights ### :rocket: Epics and highlights

View file

@ -114,37 +114,13 @@ Debug Main Page
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Import binfile:</legend> <legend>Import binfile:</legend>
<desc>Import penpot file in binary <desc>Import penpot file in binary format.</desc>
format. If <strong>overwrite</strong> is checked, all files will
be overwritten using the same ids found in the file instead of
generating a new ones.</desc>
<form method="post" enctype="multipart/form-data" action="/dbg/file/import"> <form method="post" enctype="multipart/form-data" action="/dbg/file/import">
<div class="row"> <div class="row">
<input type="file" name="file" value="" /> <input type="file" name="file" value="" />
</div> </div>
<div class="row">
<label>Overwrite?</label>
<input type="checkbox" name="overwrite" />
<br />
<small>
Instead of creating a new file with all relations remapped,
reuses all ids and updates/overwrites the objects that are
already exists on the database.
<strong>Warning, this operation should be used with caution.</strong>
</small>
</div>
<div class="row">
<label>Migrate?</label>
<input type="checkbox" name="migrate" />
<br />
<small>
Applies the file migrations on the importation process.
</small>
</div>
<div class="row"> <div class="row">
<input type="submit" name="upload" value="Upload" /> <input type="submit" name="upload" value="Upload" />
</div> </div>

View file

@ -30,7 +30,9 @@
[app.worker :as-alias wrk] [app.worker :as-alias wrk]
[clojure.set :as set] [clojure.set :as set]
[clojure.walk :as walk] [clojure.walk :as walk]
[cuerdas.core :as str])) [cuerdas.core :as str]
[datoteka.fs :as fs]
[datoteka.io :as io]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
@ -61,6 +63,20 @@
:version :version
:data}) :data})
(defn parse-file-format
[template]
(assert (fs/path? template) "expected InputStream for `template`")
(with-open [^java.lang.AutoCloseable input (io/input-stream template)]
(let [buffer (byte-array 4)]
(io/read-to-buffer input buffer)
(if (and (= (aget buffer 0) 80)
(= (aget buffer 1) 75)
(= (aget buffer 2) 3)
(= (aget buffer 3) 4))
:binfile-v3
:binfile-v1))))
(def xf-map-id (def xf-map-id
(map :id)) (map :id))

View file

@ -298,7 +298,7 @@
(defmulti write-section ::section) (defmulti write-section ::section)
(defn write-export! (defn write-export!
[{:keys [::include-libraries ::embed-assets] :as cfg}] [{:keys [::bfc/include-libraries ::bfc/embed-assets] :as cfg}]
(when (and include-libraries embed-assets) (when (and include-libraries embed-assets)
(throw (IllegalArgumentException. (throw (IllegalArgumentException.
"the `include-libraries` and `embed-assets` are mutally excluding options"))) "the `include-libraries` and `embed-assets` are mutally excluding options")))
@ -323,7 +323,7 @@
[:v1/metadata :v1/files :v1/rels :v1/sobjects])))) [:v1/metadata :v1/files :v1/rels :v1/sobjects]))))
(defmethod write-section :v1/metadata (defmethod write-section :v1/metadata
[{:keys [::output ::ids ::include-libraries] :as cfg}] [{:keys [::output ::bfc/ids ::bfc/include-libraries] :as cfg}]
(if-let [fids (get-files cfg ids)] (if-let [fids (get-files cfg ids)]
(let [lids (when include-libraries (let [lids (when include-libraries
(bfc/get-libraries cfg ids)) (bfc/get-libraries cfg ids))
@ -335,7 +335,7 @@
:hint "unable to retrieve files for export"))) :hint "unable to retrieve files for export")))
(defmethod write-section :v1/files (defmethod write-section :v1/files
[{:keys [::output ::embed-assets ::include-libraries] :as cfg}] [{:keys [::output ::bfc/embed-assets ::bfc/include-libraries] :as cfg}]
;; Initialize SIDS with empty vector ;; Initialize SIDS with empty vector
(vswap! bfc/*state* assoc :sids []) (vswap! bfc/*state* assoc :sids [])
@ -382,7 +382,7 @@
(vswap! bfc/*state* update :sids into bfc/xf-map-media-id thumbnails)))) (vswap! bfc/*state* update :sids into bfc/xf-map-media-id thumbnails))))
(defmethod write-section :v1/rels (defmethod write-section :v1/rels
[{:keys [::output ::include-libraries] :as cfg}] [{:keys [::output ::bfc/include-libraries] :as cfg}]
(let [ids (-> bfc/*state* deref :files set) (let [ids (-> bfc/*state* deref :files set)
rels (when include-libraries rels (when include-libraries
(bfc/get-files-rels cfg ids))] (bfc/get-files-rels cfg ids))]
@ -421,15 +421,15 @@
(defmulti read-import ::version) (defmulti read-import ::version)
(defmulti read-section ::section) (defmulti read-section ::section)
(s/def ::profile-id ::us/uuid) (s/def ::bfc/profile-id ::us/uuid)
(s/def ::project-id ::us/uuid) (s/def ::bfc/project-id ::us/uuid)
(s/def ::input io/input-stream?) (s/def ::bfc/input io/input-stream?)
(s/def ::overwrite? (s/nilable ::us/boolean)) (s/def ::overwrite? (s/nilable ::us/boolean))
(s/def ::ignore-index-errors? (s/nilable ::us/boolean)) (s/def ::ignore-index-errors? (s/nilable ::us/boolean))
;; FIXME: replace with schema ;; FIXME: replace with schema
(s/def ::read-import-options (s/def ::read-import-options
(s/keys :req [::db/pool ::sto/storage ::project-id ::profile-id ::input] (s/keys :req [::db/pool ::sto/storage ::bfc/project-id ::bfc/profile-id ::bfc/input]
:opt [::overwrite? ::ignore-index-errors?])) :opt [::overwrite? ::ignore-index-errors?]))
(defn read-import! (defn read-import!
@ -439,7 +439,7 @@
`::bfc/overwrite`: if true, instead of creating new files and remapping id references, `::bfc/overwrite`: if true, instead of creating new files and remapping id references,
it reuses all ids and updates existing objects; defaults to `false`." it reuses all ids and updates existing objects; defaults to `false`."
[{:keys [::input ::bfc/timestamp] :or {timestamp (dt/now)} :as options}] [{:keys [::bfc/input ::bfc/timestamp] :or {timestamp (dt/now)} :as options}]
(dm/assert! (dm/assert!
"expected input stream" "expected input stream"
@ -453,7 +453,7 @@
(read-import (assoc options ::version version ::bfc/timestamp timestamp)))) (read-import (assoc options ::version version ::bfc/timestamp timestamp))))
(defn- read-import-v1 (defn- read-import-v1
[{:keys [::db/conn ::project-id ::profile-id ::input] :as cfg}] [{:keys [::db/conn ::bfc/project-id ::bfc/profile-id ::bfc/input] :as cfg}]
(bfc/disable-database-timeouts! cfg) (bfc/disable-database-timeouts! cfg)
@ -473,7 +473,7 @@
(let [options (-> cfg (let [options (-> cfg
(assoc ::bfc/features features) (assoc ::bfc/features features)
(assoc ::section section) (assoc ::section section)
(assoc ::input input))] (assoc ::bfc/input input))]
(binding [bfc/*options* options] (binding [bfc/*options* options]
(events/tap :progress {:op :import :section section}) (events/tap :progress {:op :import :section section})
(read-section options)))) (read-section options))))
@ -491,7 +491,7 @@
(db/tx-run! options read-import-v1)) (db/tx-run! options read-import-v1))
(defmethod read-section :v1/metadata (defmethod read-section :v1/metadata
[{:keys [::input]}] [{:keys [::bfc/input]}]
(let [{:keys [version files]} (read-obj! input)] (let [{:keys [version files]} (read-obj! input)]
(l/dbg :hint "metadata readed" (l/dbg :hint "metadata readed"
:version (:full version) :version (:full version)
@ -509,7 +509,7 @@
thumbnails)) thumbnails))
(defmethod read-section :v1/files (defmethod read-section :v1/files
[{:keys [::db/conn ::input ::project-id ::bfc/overwrite ::name] :as system}] [{:keys [::db/conn ::bfc/input ::bfc/project-id ::bfc/overwrite ::bfc/name] :as system}]
(doseq [[idx expected-file-id] (d/enumerate (-> bfc/*state* deref :files))] (doseq [[idx expected-file-id] (d/enumerate (-> bfc/*state* deref :files))]
(let [file (read-obj! input) (let [file (read-obj! input)
@ -576,7 +576,7 @@
file-id')))) file-id'))))
(defmethod read-section :v1/rels (defmethod read-section :v1/rels
[{:keys [::db/conn ::input ::bfc/timestamp]}] [{:keys [::db/conn ::bfc/input ::bfc/timestamp]}]
(let [rels (read-obj! input) (let [rels (read-obj! input)
ids (into #{} (-> bfc/*state* deref :files))] ids (into #{} (-> bfc/*state* deref :files))]
;; Insert all file relations ;; Insert all file relations
@ -600,7 +600,7 @@
::l/sync? true)))))) ::l/sync? true))))))
(defmethod read-section :v1/sobjects (defmethod read-section :v1/sobjects
[{:keys [::db/conn ::input ::bfc/overwrite ::bfc/timestamp] :as cfg}] [{:keys [::db/conn ::bfc/input ::bfc/overwrite ::bfc/timestamp] :as cfg}]
(let [storage (sto/resolve cfg) (let [storage (sto/resolve cfg)
ids (read-obj! input) ids (read-obj! input)
thumb? (into #{} (map :media-id) (:thumbnails @bfc/*state*))] thumb? (into #{} (map :media-id) (:thumbnails @bfc/*state*))]
@ -674,17 +674,17 @@
"Do the exportation of a specified file in custom penpot binary "Do the exportation of a specified file in custom penpot binary
format. There are some options available for customize the output: format. There are some options available for customize the output:
`::include-libraries`: additionally to the specified file, all the `::bfc/include-libraries`: additionally to the specified file, all the
linked libraries also will be included (including transitive linked libraries also will be included (including transitive
dependencies). dependencies).
`::embed-assets`: instead of including the libraries, embed in the `::bfc/embed-assets`: instead of including the libraries, embed in the
same file library all assets used from external libraries." same file library all assets used from external libraries."
[{:keys [::ids] :as cfg} output] [{:keys [::bfc/ids] :as cfg} output]
(dm/assert! (dm/assert!
"expected a set of uuid's for `::ids` parameter" "expected a set of uuid's for `::bfc/ids` parameter"
(and (set? ids) (and (set? ids)
(every? uuid? ids))) (every? uuid? ids)))
@ -719,12 +719,12 @@
:cause @cs))))) :cause @cs)))))
(defn import-files! (defn import-files!
[{:keys [::input] :as cfg}] [{:keys [::bfc/input] :as cfg}]
(dm/assert! (dm/assert!
"expected valid profile-id and project-id on `cfg`" "expected valid profile-id and project-id on `cfg`"
(and (uuid? (::profile-id cfg)) (and (uuid? (::bfc/profile-id cfg))
(uuid? (::project-id cfg)))) (uuid? (::bfc/project-id cfg))))
(dm/assert! (dm/assert!
"expected instance of jio/IOFactory for `input`" "expected instance of jio/IOFactory for `input`"
@ -738,7 +738,7 @@
(try (try
(binding [*position* (atom 0)] (binding [*position* (atom 0)]
(pu/with-open [input (io/input-stream input)] (pu/with-open [input (io/input-stream input)]
(read-import! (assoc cfg ::input input)))) (read-import! (assoc cfg ::bfc/input input))))
(catch ZstdIOException cause (catch ZstdIOException cause
(ex/raise :type :validation (ex/raise :type :validation

View file

@ -206,7 +206,7 @@
(.closeEntry output)) (.closeEntry output))
(defn- get-file (defn- get-file
[{:keys [::embed-assets ::include-libraries] :as cfg} file-id] [{:keys [::bfc/embed-assets ::bfc/include-libraries] :as cfg} file-id]
(when (and include-libraries embed-assets) (when (and include-libraries embed-assets)
(throw (IllegalArgumentException. (throw (IllegalArgumentException.
@ -354,7 +354,7 @@
(write-entry! output path encoded-tokens))))) (write-entry! output path encoded-tokens)))))
(defn- export-files (defn- export-files
[{:keys [::ids ::include-libraries ::output] :as cfg}] [{:keys [::bfc/ids ::bfc/include-libraries ::output] :as cfg}]
(let [ids (into ids (when include-libraries (bfc/get-libraries cfg ids))) (let [ids (into ids (when include-libraries (bfc/get-libraries cfg ids)))
rels (if include-libraries rels (if include-libraries
(->> (bfc/get-files-rels cfg ids) (->> (bfc/get-files-rels cfg ids)
@ -546,7 +546,7 @@
(json/read reader))) (json/read reader)))
(defn- read-file (defn- read-file
[{:keys [::input ::file-id]}] [{:keys [::bfc/input ::file-id]}]
(let [path (str "files/" file-id ".json") (let [path (str "files/" file-id ".json")
entry (get-zip-entry input path)] entry (get-zip-entry input path)]
(-> (read-entry input entry) (-> (read-entry input entry)
@ -554,7 +554,7 @@
(validate-file)))) (validate-file))))
(defn- read-file-plugin-data (defn- read-file-plugin-data
[{:keys [::input ::file-id]}] [{:keys [::bfc/input ::file-id]}]
(let [path (str "files/" file-id "/plugin-data.json") (let [path (str "files/" file-id "/plugin-data.json")
entry (get-zip-entry* input path)] entry (get-zip-entry* input path)]
(some->> entry (some->> entry
@ -563,7 +563,7 @@
(validate-plugin-data)))) (validate-plugin-data))))
(defn- read-file-media (defn- read-file-media
[{:keys [::input ::file-id ::entries]}] [{:keys [::bfc/input ::file-id ::entries]}]
(->> (keep (match-media-entry-fn file-id) entries) (->> (keep (match-media-entry-fn file-id) entries)
(reduce (fn [result {:keys [id entry]}] (reduce (fn [result {:keys [id entry]}]
(let [object (->> (read-entry input entry) (let [object (->> (read-entry input entry)
@ -577,7 +577,7 @@
(not-empty))) (not-empty)))
(defn- read-file-colors (defn- read-file-colors
[{:keys [::input ::file-id ::entries]}] [{:keys [::bfc/input ::file-id ::entries]}]
(->> (keep (match-color-entry-fn file-id) entries) (->> (keep (match-color-entry-fn file-id) entries)
(reduce (fn [result {:keys [id entry]}] (reduce (fn [result {:keys [id entry]}]
(let [object (->> (read-entry input entry) (let [object (->> (read-entry input entry)
@ -590,7 +590,7 @@
(not-empty))) (not-empty)))
(defn- read-file-components (defn- read-file-components
[{:keys [::input ::file-id ::entries]}] [{:keys [::bfc/input ::file-id ::entries]}]
(->> (keep (match-component-entry-fn file-id) entries) (->> (keep (match-component-entry-fn file-id) entries)
(reduce (fn [result {:keys [id entry]}] (reduce (fn [result {:keys [id entry]}]
(let [object (->> (read-entry input entry) (let [object (->> (read-entry input entry)
@ -603,7 +603,7 @@
(not-empty))) (not-empty)))
(defn- read-file-typographies (defn- read-file-typographies
[{:keys [::input ::file-id ::entries]}] [{:keys [::bfc/input ::file-id ::entries]}]
(->> (keep (match-typography-entry-fn file-id) entries) (->> (keep (match-typography-entry-fn file-id) entries)
(reduce (fn [result {:keys [id entry]}] (reduce (fn [result {:keys [id entry]}]
(let [object (->> (read-entry input entry) (let [object (->> (read-entry input entry)
@ -623,7 +623,7 @@
(validate-tokens-lib)))) (validate-tokens-lib))))
(defn- read-file-shapes (defn- read-file-shapes
[{:keys [::input ::file-id ::page-id ::entries] :as cfg}] [{:keys [::bfc/input ::file-id ::page-id ::entries] :as cfg}]
(->> (keep (match-shape-entry-fn file-id page-id) entries) (->> (keep (match-shape-entry-fn file-id page-id) entries)
(reduce (fn [result {:keys [id entry]}] (reduce (fn [result {:keys [id entry]}]
(let [object (->> (read-entry input entry) (let [object (->> (read-entry input entry)
@ -636,7 +636,7 @@
(not-empty))) (not-empty)))
(defn- read-file-pages (defn- read-file-pages
[{:keys [::input ::file-id ::entries] :as cfg}] [{:keys [::bfc/input ::file-id ::entries] :as cfg}]
(->> (keep (match-page-entry-fn file-id) entries) (->> (keep (match-page-entry-fn file-id) entries)
(keep (fn [{:keys [id entry]}] (keep (fn [{:keys [id entry]}]
(let [page (->> (read-entry input entry) (let [page (->> (read-entry input entry)
@ -652,7 +652,7 @@
(d/ordered-map)))) (d/ordered-map))))
(defn- read-file-thumbnails (defn- read-file-thumbnails
[{:keys [::input ::file-id ::entries] :as cfg}] [{:keys [::bfc/input ::file-id ::entries] :as cfg}]
(->> (keep (match-thumbnail-entry-fn file-id) entries) (->> (keep (match-thumbnail-entry-fn file-id) entries)
(reduce (fn [result {:keys [page-id frame-id tag entry]}] (reduce (fn [result {:keys [page-id frame-id tag entry]}]
(let [object (->> (read-entry input entry) (let [object (->> (read-entry input entry)
@ -684,7 +684,7 @@
:plugin-data plugin-data})) :plugin-data plugin-data}))
(defn- import-file (defn- import-file
[{:keys [::db/conn ::project-id ::file-id ::file-name] :as cfg}] [{:keys [::db/conn ::bfc/project-id ::file-id ::file-name] :as cfg}]
(let [file-id' (bfc/lookup-index file-id) (let [file-id' (bfc/lookup-index file-id)
file (read-file cfg) file (read-file cfg)
media (read-file-media cfg) media (read-file-media cfg)
@ -760,7 +760,7 @@
:library-file-id libr-id}))))) :library-file-id libr-id})))))
(defn- import-storage-objects (defn- import-storage-objects
[{:keys [::input ::entries ::bfc/timestamp] :as cfg}] [{:keys [::bfc/input ::entries ::bfc/timestamp] :as cfg}]
(events/tap :progress {:section :storage-objects}) (events/tap :progress {:section :storage-objects})
(let [storage (sto/resolve cfg) (let [storage (sto/resolve cfg)
@ -857,7 +857,7 @@
{::db/on-conflict-do-nothing? (::bfc/overwrite cfg)})))) {::db/on-conflict-do-nothing? (::bfc/overwrite cfg)}))))
(defn- import-files (defn- import-files
[{:keys [::bfc/timestamp ::input ::name] :or {timestamp (dt/now)} :as cfg}] [{:keys [::bfc/timestamp ::bfc/input ::bfc/name] :or {timestamp (dt/now)} :as cfg}]
(dm/assert! (dm/assert!
"expected zip file" "expected zip file"
@ -925,17 +925,17 @@
"Do the exportation of a specified file in custom penpot binary "Do the exportation of a specified file in custom penpot binary
format. There are some options available for customize the output: format. There are some options available for customize the output:
`::include-libraries`: additionally to the specified file, all the `::bfc/include-libraries`: additionally to the specified file, all the
linked libraries also will be included (including transitive linked libraries also will be included (including transitive
dependencies). dependencies).
`::embed-assets`: instead of including the libraries, embed in the `::bfc/embed-assets`: instead of including the libraries, embed in the
same file library all assets used from external libraries." same file library all assets used from external libraries."
[{:keys [::ids] :as cfg} output] [{:keys [::bfc/ids] :as cfg} output]
(dm/assert! (dm/assert!
"expected a set of uuid's for `::ids` parameter" "expected a set of uuid's for `::bfc/ids` parameter"
(and (set? ids) (and (set? ids)
(every? uuid? ids))) (every? uuid? ids)))
@ -977,14 +977,13 @@
:aborted @ab :aborted @ab
:cause @cs))))) :cause @cs)))))
(defn import-files! (defn import-files!
[{:keys [::input] :as cfg}] [{:keys [::bfc/input] :as cfg}]
(dm/assert! (dm/assert!
"expected valid profile-id and project-id on `cfg`" "expected valid profile-id and project-id on `cfg`"
(and (uuid? (::profile-id cfg)) (and (uuid? (::bfc/profile-id cfg))
(uuid? (::project-id cfg)))) (uuid? (::bfc/project-id cfg))))
(dm/assert! (dm/assert!
"expected instance of jio/IOFactory for `input`" "expected instance of jio/IOFactory for `input`"
@ -997,7 +996,7 @@
(l/info :hint "import: started" :id (str id)) (l/info :hint "import: started" :id (str id))
(try (try
(with-open [input (ZipFile. (fs/file input))] (with-open [input (ZipFile. (fs/file input))]
(import-files (assoc cfg ::input input))) (import-files (assoc cfg ::bfc/input input)))
(catch Throwable cause (catch Throwable cause
(vreset! cs cause) (vreset! cs cause)

View file

@ -7,7 +7,9 @@
(ns app.http.debug (ns app.http.debug
(:refer-clojure :exclude [error-handler]) (:refer-clojure :exclude [error-handler])
(:require (:require
[app.binfile.common :as bfc]
[app.binfile.v1 :as bf.v1] [app.binfile.v1 :as bf.v1]
[app.binfile.v3 :as bf.v3]
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
@ -280,23 +282,23 @@
(ex/raise :type :validation (ex/raise :type :validation
:code :missing-arguments)) :code :missing-arguments))
(let [path (tmp/tempfile :prefix "penpot.export.")] (let [path (tmp/tempfile :prefix "penpot.export." :min-age "30m")]
(with-open [output (io/output-stream path)] (with-open [output (io/output-stream path)]
(-> cfg (-> cfg
(assoc ::bf.v1/ids file-ids) (assoc ::bfc/ids file-ids)
(assoc ::bf.v1/embed-assets embed?) (assoc ::bfc/embed-assets embed?)
(assoc ::bf.v1/include-libraries libs?) (assoc ::bfc/include-libraries libs?)
(bf.v1/export-files! output))) (bf.v3/export-files! output)))
(if clone? (if clone?
(let [profile (profile/get-profile pool profile-id) (let [profile (profile/get-profile pool profile-id)
project-id (:default-project-id profile) project-id (:default-project-id profile)
cfg (assoc cfg cfg (assoc cfg
::bf.v1/overwrite false ::bfc/overwrite false
::bf.v1/profile-id profile-id ::bfc/profile-id profile-id
::bf.v1/project-id project-id ::bfc/project-id project-id
::bf.v1/input path)] ::bfc/input path)]
(bf.v1/import-files! cfg) (bf.v3/import-files! cfg)
{::yres/status 200 {::yres/status 200
::yres/headers {"content-type" "text/plain"} ::yres/headers {"content-type" "text/plain"}
::yres/body "OK CLONED"}) ::yres/body "OK CLONED"})
@ -315,23 +317,24 @@
:hint "missing upload file")) :hint "missing upload file"))
(let [profile (profile/get-profile pool profile-id) (let [profile (profile/get-profile pool profile-id)
project-id (:default-project-id profile) project-id (:default-project-id profile)]
overwrite? (contains? params :overwrite)
migrate? (contains? params :migrate)]
(when-not project-id (when-not project-id
(ex/raise :type :validation (ex/raise :type :validation
:code :missing-project :code :missing-project
:hint "project not found")) :hint "project not found"))
(let [path (-> params :file :path) (let [path (-> params :file :path)
cfg (assoc cfg format (bfc/parse-file-format path)
::bf.v1/overwrite overwrite? cfg (assoc cfg
::bf.v1/migrate migrate? ::bfc/profile-id profile-id
::bf.v1/profile-id profile-id ::bfc/project-id project-id
::bf.v1/project-id project-id ::bfc/input path)]
::bf.v1/input path)]
(bf.v1/import-files! cfg) (if (= format :binfile-v3)
(bf.v3/import-files! cfg)
(bf.v1/import-files! cfg))
{::yres/status 200 {::yres/status 200
::yres/headers {"content-type" "text/plain"} ::yres/headers {"content-type" "text/plain"}
::yres/body "OK"}))) ::yres/body "OK"})))

View file

@ -64,7 +64,8 @@
(catch Throwable cause (catch Throwable cause
(events/tap :error (errors/handle' cause request)) (events/tap :error (errors/handle' cause request))
(when-not (ex/instance? java.io.EOFException cause) (when-not (ex/instance? java.io.EOFException cause)
(l/err :hint "unexpected error on processing sse response" :cause cause))) (binding [l/*context* (errors/request->context request)]
(l/err :hint "unexpected error on processing sse response" :cause cause))))
(finally (finally
(sp/close! events/*channel*) (sp/close! events/*channel*)
(px/await! listener)))))))})) (px/await! listener)))))))}))

View file

@ -7,6 +7,7 @@
(ns app.rpc.commands.binfile (ns app.rpc.commands.binfile
(:refer-clojure :exclude [assert]) (:refer-clojure :exclude [assert])
(:require (:require
[app.binfile.common :as bfc]
[app.binfile.v1 :as bf.v1] [app.binfile.v1 :as bf.v1]
[app.binfile.v3 :as bf.v3] [app.binfile.v3 :as bf.v3]
[app.common.logging :as l] [app.common.logging :as l]
@ -46,9 +47,9 @@
(fn [_ output-stream] (fn [_ output-stream]
(try (try
(-> cfg (-> cfg
(assoc ::bf.v1/ids #{file-id}) (assoc ::bfc/ids #{file-id})
(assoc ::bf.v1/embed-assets embed-assets) (assoc ::bfc/embed-assets embed-assets)
(assoc ::bf.v1/include-libraries include-libraries) (assoc ::bfc/include-libraries include-libraries)
(bf.v1/export-files! output-stream)) (bf.v1/export-files! output-stream))
(catch Throwable cause (catch Throwable cause
(l/err :hint "exception on exporting file" (l/err :hint "exception on exporting file"
@ -61,9 +62,9 @@
(fn [_ output-stream] (fn [_ output-stream]
(try (try
(-> cfg (-> cfg
(assoc ::bf.v3/ids #{file-id}) (assoc ::bfc/ids #{file-id})
(assoc ::bf.v3/embed-assets embed-assets) (assoc ::bfc/embed-assets embed-assets)
(assoc ::bf.v3/include-libraries include-libraries) (assoc ::bfc/include-libraries include-libraries)
(bf.v3/export-files! output-stream)) (bf.v3/export-files! output-stream))
(catch Throwable cause (catch Throwable cause
(l/err :hint "exception on exporting file" (l/err :hint "exception on exporting file"
@ -93,10 +94,10 @@
(defn- import-binfile-v1 (defn- import-binfile-v1
[{:keys [::wrk/executor] :as cfg} {:keys [project-id profile-id name file]}] [{:keys [::wrk/executor] :as cfg} {:keys [project-id profile-id name file]}]
(let [cfg (-> cfg (let [cfg (-> cfg
(assoc ::bf.v1/project-id project-id) (assoc ::bfc/project-id project-id)
(assoc ::bf.v1/profile-id profile-id) (assoc ::bfc/profile-id profile-id)
(assoc ::bf.v1/name name) (assoc ::bfc/name name)
(assoc ::bf.v1/input (:path file)))] (assoc ::bfc/input (:path file)))]
;; NOTE: the importation process performs some operations that are ;; NOTE: the importation process performs some operations that are
;; not very friendly with virtual threads, and for avoid ;; not very friendly with virtual threads, and for avoid
@ -107,10 +108,10 @@
(defn- import-binfile-v3 (defn- import-binfile-v3
[{:keys [::wrk/executor] :as cfg} {:keys [project-id profile-id name file]}] [{:keys [::wrk/executor] :as cfg} {:keys [project-id profile-id name file]}]
(let [cfg (-> cfg (let [cfg (-> cfg
(assoc ::bf.v3/project-id project-id) (assoc ::bfc/project-id project-id)
(assoc ::bf.v3/profile-id profile-id) (assoc ::bfc/profile-id profile-id)
(assoc ::bf.v3/name name) (assoc ::bfc/name name)
(assoc ::bf.v3/input (:path file)))] (assoc ::bfc/input (:path file)))]
;; NOTE: the importation process performs some operations that are ;; NOTE: the importation process performs some operations that are
;; not very friendly with virtual threads, and for avoid ;; not very friendly with virtual threads, and for avoid
;; unexpected blocking of other concurrent operations we dispatch ;; unexpected blocking of other concurrent operations we dispatch

View file

@ -9,6 +9,7 @@
(:require (:require
[app.binfile.common :as bfc] [app.binfile.common :as bfc]
[app.binfile.v1 :as bf.v1] [app.binfile.v1 :as bf.v1]
[app.binfile.v3 :as bf.v3]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.features :as cfeat] [app.common.features :as cfeat]
[app.common.schema :as sm] [app.common.schema :as sm]
@ -25,6 +26,7 @@
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.setup :as-alias setup] [app.setup :as-alias setup]
[app.setup.templates :as tmpl] [app.setup.templates :as tmpl]
[app.storage.tmp :as tmp]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as-alias wrk] [app.worker :as-alias wrk]
@ -400,11 +402,20 @@
;; that are not very friendly with virtual threads, and for ;; that are not very friendly with virtual threads, and for
;; avoid unexpected blocking of other concurrent operations ;; avoid unexpected blocking of other concurrent operations
;; we dispatch that operation to a dedicated executor. ;; we dispatch that operation to a dedicated executor.
(let [cfg (-> cfg (let [template (tmp/tempfile-from template
(assoc ::bf.v1/project-id project-id) :prefix "penpot.template."
(assoc ::bf.v1/profile-id profile-id) :suffix ""
(assoc ::bf.v1/input template)) :min-age "30m")
result (px/invoke! executor (partial bf.v1/import-files! cfg))] format (bfc/parse-file-format template)
cfg (-> cfg
(assoc ::bfc/project-id project-id)
(assoc ::bfc/profile-id profile-id)
(assoc ::bfc/input template))
result (if (= format :binfile-v3)
(px/invoke! executor (partial bf.v3/import-files! cfg))
(px/invoke! executor (partial bf.v1/import-files! cfg)))]
(db/update! conn :project (db/update! conn :project
{:modified-at (dt/now)} {:modified-at (dt/now)}

View file

@ -54,6 +54,7 @@
(::setup/templates cfg))] (::setup/templates cfg))]
(let [dest (fs/join fs/*cwd* "builtin-templates") (let [dest (fs/join fs/*cwd* "builtin-templates")
path (or (:path template) (fs/join dest template-id))] path (or (:path template) (fs/join dest template-id))]
(if (fs/exists? path) (if (fs/exists? path)
(io/input-stream path) (io/input-stream path)
(let [resp (http/req! cfg (let [resp (http/req! cfg

View file

@ -16,10 +16,13 @@
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk] [app.worker :as wrk]
[datoteka.fs :as fs] [datoteka.fs :as fs]
[datoteka.io :as io]
[integrant.core :as ig] [integrant.core :as ig]
[promesa.exec :as px] [promesa.exec :as px]
[promesa.exec.csp :as sp]) [promesa.exec.csp :as sp])
(:import (:import
java.io.InputStream
java.io.OutputStream
java.nio.file.Files)) java.nio.file.Files))
(def default-tmp-dir "/tmp/penpot") (def default-tmp-dir "/tmp/penpot")
@ -86,3 +89,12 @@
(fs/delete-on-exit! path) (fs/delete-on-exit! path)
(sp/offer! queue [path (some-> min-age dt/duration)]) (sp/offer! queue [path (some-> min-age dt/duration)])
path)) path))
(defn tempfile-from
"Create a new tempfile from from consuming the stream"
[input & {:as options}]
(let [path (tempfile options)]
(with-open [^InputStream input (io/input-stream input)]
(with-open [^OutputStream output (io/output-stream path)]
(io/copy input output)))
path))

View file

@ -7,6 +7,7 @@
(ns backend-tests.binfile-test (ns backend-tests.binfile-test
"Internal binfile test, no RPC involved" "Internal binfile test, no RPC involved"
(:require (:require
[app.binfile.common :as bfc]
[app.binfile.v3 :as v3] [app.binfile.v3 :as v3]
[app.common.features :as cfeat] [app.common.features :as cfeat]
[app.common.pprint :as pp] [app.common.pprint :as pp]
@ -93,15 +94,15 @@
(v3/export-files! (v3/export-files!
(-> th/*system* (-> th/*system*
(assoc ::v3/ids #{(:id file)}) (assoc ::bfc/ids #{(:id file)})
(assoc ::v3/embed-assets false) (assoc ::bfc/embed-assets false)
(assoc ::v3/include-libraries false)) (assoc ::bfc/include-libraries false))
(io/output-stream output)) (io/output-stream output))
(let [result (-> th/*system* (let [result (-> th/*system*
(assoc ::v3/project-id (:default-project-id profile)) (assoc ::bfc/project-id (:default-project-id profile))
(assoc ::v3/profile-id (:id profile)) (assoc ::bfc/profile-id (:id profile))
(assoc ::v3/input output) (assoc ::bfc/input output)
(v3/import-files!))] (v3/import-files!))]
(t/is (= (count result) 1)) (t/is (= (count result) 1))
(t/is (every? uuid? result))))) (t/is (every? uuid? result)))))

View file

@ -7,6 +7,14 @@ title: 2. Create a Plugin
This guide covers the creation of a Penpot plugin. Penpot offers two ways to kickstart your development: This guide covers the creation of a Penpot plugin. Penpot offers two ways to kickstart your development:
<p class="advice">
Have you got an idea for a new plugin? Great! But first take a look at <a
href="https://penpot.app/penpothub/plugins">the plugin overview</a> to see if already
exists, and consider joining efforts with other developers. This does not imply that we
won't accept plugins that do similar things, since anything can be improved and done in
different ways.
</p>
1. Using a Template: 1. Using a Template:
- **Typescript template**: Using the <a target="_blank" href="https://github.com/penpot/penpot-plugin-starter-template">Penpot Plugin Starter Template</a>: A basic template with the required files for quickstarting your plugin. This template uses Typescript and Vite. - **Typescript template**: Using the <a target="_blank" href="https://github.com/penpot/penpot-plugin-starter-template">Penpot Plugin Starter Template</a>: A basic template with the required files for quickstarting your plugin. This template uses Typescript and Vite.

View file

@ -405,6 +405,14 @@ where users will access the application:
PENPOT_PUBLIC_URI: http://localhost:9001 PENPOT_PUBLIC_URI: http://localhost:9001
``` ```
<p class="advice">
If you plan to serve Penpot under different domain than `localhost` without HTTPS,
you need to disable the `secure` flag on cookies, with the `disable-secure-session-cookies` flag.
This is a configuration NOT recommended for production environments.
</p>
Check all the [flags](#other-flags) to fully customize your instance.
## Frontend ## ## Frontend ##
In comparison with backend, frontend only has a small number of runtime configuration In comparison with backend, frontend only has a small number of runtime configuration
@ -424,8 +432,8 @@ To connect the frontend to the exporter and backend, you need to fill out these
```bash ```bash
# Frontend # Frontend
PENPOT_BACKEND_URI: http://your-penpot-backend PENPOT_BACKEND_URI: http://your-penpot-backend:6060
PENPOT_EXPORTER_URI: http://your-penpot-exporter PENPOT_EXPORTER_URI: http://your-penpot-exporter:6061
``` ```
These variables are used for generate correct nginx.conf file on container startup. These variables are used for generate correct nginx.conf file on container startup.
@ -480,3 +488,4 @@ __Since version 2.0.0__
[2]: /technical-guide/getting-started#configure-penpot-with-docker [2]: /technical-guide/getting-started#configure-penpot-with-docker
[3]: /technical-guide/developer/common#dev-environment [3]: /technical-guide/developer/common#dev-environment
[4]: https://github.com/penpot/penpot/blob/main/docker/images/files/nginx.conf [4]: https://github.com/penpot/penpot/blob/main/docker/images/files/nginx.conf

View file

@ -227,6 +227,9 @@ docker compose -f docker-compose.yaml pull
This will fetch the latest images. When you do <code class="language-bash">docker compose up</code> again, the containers will be recreated with the latest version. This will fetch the latest images. When you do <code class="language-bash">docker compose up</code> again, the containers will be recreated with the latest version.
<p class="advice">
It is strongly recommended to update the Penpot version in small increments, rather than updating between two distant versions.
</p>
**Important: Upgrade from version 1.x to 2.0** **Important: Upgrade from version 1.x to 2.0**

View file

@ -12,7 +12,6 @@
[app.common.logic.shapes :as cls] [app.common.logic.shapes :as cls]
[app.common.types.shape :as cts] [app.common.types.shape :as cts]
[app.common.types.shape.layout :as ctl] [app.common.types.shape.layout :as ctl]
[app.common.types.tokens-lib :as ctob]
[app.main.constants :refer [size-presets]] [app.main.constants :refer [size-presets]]
[app.main.data.workspace :as udw] [app.main.data.workspace :as udw]
[app.main.data.workspace.interactions :as dwi] [app.main.data.workspace.interactions :as dwi]
@ -24,15 +23,10 @@
[app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.numeric-input :refer [numeric-input*]] [app.main.ui.components.numeric-input :refer [numeric-input*]]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.context :as muc]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.hooks :as hooks] [app.main.ui.hooks :as hooks]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.workspace.sidebar.options.menus.border-radius :refer [border-radius-menu]] [app.main.ui.workspace.sidebar.options.menus.border-radius :refer [border-radius-menu]]
[app.main.ui.workspace.tokens.core :as wtc]
[app.main.ui.workspace.tokens.editable-select :refer [editable-select]]
[app.main.ui.workspace.tokens.style-dictionary :as sd]
[app.main.ui.workspace.tokens.token-types :as wtty]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[clojure.set :as set] [clojure.set :as set]
@ -101,9 +95,7 @@
{::mf/props :obj {::mf/props :obj
::mf/wrap [mf/memo]} ::mf/wrap [mf/memo]}
[{:keys [ids ids-with-children values type all-types shape]}] [{:keys [ids ids-with-children values type all-types shape]}]
(let [design-tokens? (mf/use-ctx muc/design-tokens) (let [options
options
(mf/with-memo [type all-types] (mf/with-memo [type all-types]
(if (= type :multiple) (if (= type :multiple)
(into #{} (mapcat type->options) all-types) (into #{} (mapcat type->options) all-types)
@ -125,27 +117,6 @@
selection-parents-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids)) selection-parents-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids))
selection-parents (mf/deref selection-parents-ref) selection-parents (mf/deref selection-parents-ref)
tokens (sd/use-active-theme-sets-tokens)
tokens-by-type (mf/use-memo
(mf/deps tokens)
#(ctob/group-by-type tokens))
sizing-tokens (:sizing tokens-by-type)
width-options (mf/use-memo
(mf/deps shape sizing-tokens)
#(wtc/tokens->select-options
{:shape shape
:tokens sizing-tokens
:attributes (wtty/token-attributes :sizing)
:selected-attributes #{:width}}))
height-options (mf/use-memo
(mf/deps shape sizing-tokens)
#(wtc/tokens->select-options
{:shape shape
:tokens sizing-tokens
:attributes (wtty/token-attributes :sizing)
:selected-attributes #{:height}}))
flex-child? (->> selection-parents (some ctl/flex-layout?)) flex-child? (->> selection-parents (some ctl/flex-layout?))
absolute? (ctl/item-absolute? shape) absolute? (ctl/item-absolute? shape)
flex-container? (ctl/flex-layout? shape) flex-container? (ctl/flex-layout? shape)
@ -252,22 +223,9 @@
(mf/use-fn (mf/use-fn
(mf/deps ids) (mf/deps ids)
(fn [value attr] (fn [value attr]
(let [token-value (wtc/maybe-resolve-token-value value) (binding [cts/*wasm-sync* true]
undo-id (js/Symbol)] (st/emit! (udw/trigger-bounding-box-cloaking ids)
(binding [cts/*wasm-sync* true] (udw/update-dimensions ids attr value)))))
(if-not design-tokens?
(st/emit! (udw/trigger-bounding-box-cloaking ids)
(udw/update-dimensions ids attr (or token-value value)))
(st/emit! (udw/trigger-bounding-box-cloaking ids)
(dwu/start-undo-transaction undo-id)
(dwsh/update-shapes ids
(if token-value
#(assoc-in % [:applied-tokens attr] (:id value))
#(d/dissoc-in % [:applied-tokens attr]))
{:reg-objects? true
:attrs [:applied-tokens]})
(udw/update-dimensions ids attr (or token-value value))
(dwu/commit-undo-transaction undo-id)))))))
on-proportion-lock-change on-proportion-lock-change
(mf/use-fn (mf/use-fn
@ -392,50 +350,24 @@
:disabled disabled-width-sizing?) :disabled disabled-width-sizing?)
:title (tr "workspace.options.width")} :title (tr "workspace.options.width")}
[:span {:class (stl/css :icon-text)} "W"] [:span {:class (stl/css :icon-text)} "W"]
(if-not design-tokens? [:> numeric-input* {:min 0.01
[:> numeric-input* {:min 0.01 :no-validate true
:no-validate true :placeholder (if (= :multiple (:width values)) (tr "settings.multiple") "--")
:placeholder (if (= :multiple (:width values)) (tr "settings.multiple") "--") :on-change on-width-change
:on-change on-width-change :disabled disabled-width-sizing?
:disabled disabled-width-sizing? :class (stl/css :numeric-input)
:class (stl/css :numeric-input) :value (:width values)}]]
:value (:width values)}]
[:& editable-select
{:placeholder (if (= :multiple (:r1 values)) (tr "settings.multiple") "--")
:class (stl/css :token-select)
:disabled disabled-width-sizing?
:on-change on-width-change
:on-token-remove #(on-width-change (wtc/maybe-resolve-token-value %))
:options width-options
:position :left
:value (:width values)
:input-props {:type "number"
:no-validate true
:min 0.01}}])]
[:div {:class (stl/css-case :height true [:div {:class (stl/css-case :height true
:disabled disabled-height-sizing?) :disabled disabled-height-sizing?)
:title (tr "workspace.options.height")} :title (tr "workspace.options.height")}
[:span {:class (stl/css :icon-text)} "H"] [:span {:class (stl/css :icon-text)} "H"]
(if-not design-tokens? [:> numeric-input* {:min 0.01
[:> numeric-input* {:min 0.01 :no-validate true
:no-validate true :placeholder (if (= :multiple (:height values)) (tr "settings.multiple") "--")
:placeholder (if (= :multiple (:height values)) (tr "settings.multiple") "--") :on-change on-height-change
:on-change on-height-change :disabled disabled-height-sizing?
:disabled disabled-height-sizing? :class (stl/css :numeric-input)
:class (stl/css :numeric-input) :value (:height values)}]]
:value (:height values)}]
[:& editable-select
{:placeholder (if (= :multiple (:r1 values)) (tr "settings.multiple") "--")
:class (stl/css :token-select)
:disabled disabled-height-sizing?
:on-change on-height-change
:on-token-remove #(on-height-change (wtc/maybe-resolve-token-value %))
:options height-options
:position :right
:value (:height values)
:input-props {:type "number"
:no-validate true
:min 0.01}}])]
[:button {:class (stl/css-case [:button {:class (stl/css-case
:lock-size-btn true :lock-size-btn true
:selected (true? proportion-lock) :selected (true? proportion-lock)