Added option to change the name in the setup, show filename in title for downloads, use sessionStorage instead of localStorage for e2e decryption, replaced expiry image with dynamic SVG

This commit is contained in:
Marc Ole Bulling
2023-05-13 14:33:37 +02:00
parent 1e75b9641d
commit 59271c2ff2
19 changed files with 150 additions and 63 deletions

24
.readthedocs.yaml Normal file
View File

@@ -0,0 +1,24 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.11"
# You can also specify other tool versions:
# nodejs: "19"
# rust: "1.64"
# golang: "1.19"
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
# Optionally declare the Python requirements required to build your docs
python:
system_packages: true

View File

@@ -30,7 +30,7 @@ import (
// versionGokapi is the current version in readable form.
// Other version numbers can be modified in /build/go-generate/updateVersionNumbers.go
const versionGokapi = "1.7.1"
const versionGokapi = "1.7.2"
// The following calls update the version numbers, update documentation, minify Js/CSS and build the WASM modules
//go:generate go run "../../build/go-generate/updateVersionNumbers.go"

View File

@@ -147,4 +147,3 @@ By default, all files are included in the executable. If you want to change the
3. Make changes to the folders. ``static`` contains images, CSS files and JavaScript. ``templates`` contains the HTML code.
4. Restart the server. If the folders exist, the server will use the local files instead of the embedded files.
5. Optional: To embed the files permanently, copy the modified files back to the original folders and recompile with ``go build Gokapi/cmd/gokapi``.

View File

@@ -60,6 +60,9 @@ func Load() {
if envMaxMem != "" {
serverSettings.MaxMemory = Environment.MaxMemory
}
if serverSettings.PublicName == "" {
serverSettings.PublicName = "Gokapi"
}
helper.CreateDir(serverSettings.DataDir)
filesystem.Init(serverSettings.DataDir)
log.Init(Environment.DataDir)

View File

@@ -16,7 +16,7 @@ import (
)
// CurrentConfigVersion is the version of the configuration structure. Used for upgrading
const CurrentConfigVersion = 13
const CurrentConfigVersion = 14
// DoUpgrade checks if an old version is present and updates it to the current version if required
func DoUpgrade(settings *models.Configuration, env *environment.Environment) bool {
@@ -66,6 +66,10 @@ func updateConfig(settings *models.Configuration, env *environment.Environment)
}
}
}
// < v1.7.2
if settings.ConfigVersion < 14 {
settings.PublicName = "Gokapi"
}
}
func legacyFileToCurrentFile(input []byte) models.File {

View File

@@ -114,6 +114,11 @@ func startSetupWebserver() {
WriteTimeout: 2 * time.Minute,
Handler: mux,
}
if debugDisableAuth {
srv.Addr = "127.0.0.1:" + port
fmt.Println("Authentication is disabled by debug flag. Setup only accessible by localhost")
fmt.Println("Please open http://127.0.0.1:" + port + "/setup to setup Gokapi.")
}
fmt.Println("Please open http://" + resolveHostIp() + ":" + port + "/setup to setup Gokapi.")
listener, err := net.Listen("tcp", ":"+port)
if err != nil {
@@ -337,6 +342,10 @@ func parseServerSettings(result *models.Configuration, formObjects *[]jsonFormOb
result.Port = ":" + strconv.Itoa(port)
}
result.PublicName, err = getFormValueString(formObjects, "public_name")
if err != nil {
return err
}
result.ServerUrl, err = getFormValueString(formObjects, "url")
if err != nil {
return err

View File

@@ -458,6 +458,7 @@ type setupValues struct {
BindLocalhost setupEntry `form:"localhost_sel" isBool:"true"`
UseSsl setupEntry `form:"ssl_sel" isBool:"true"`
Port setupEntry `form:"port" isInt:"true"`
PublicName setupEntry `form:"public_name"`
ExtUrl setupEntry `form:"url"`
RedirectUrl setupEntry `form:"url_redirection"`
AuthenticationMode setupEntry `form:"authentication_sel" isInt:"true"`
@@ -572,6 +573,7 @@ func createInputInternalAuth() setupValues {
values.init()
values.BindLocalhost.Value = "1"
values.PublicName.Value = "Test Name"
values.UseSsl.Value = "0"
values.Port.Value = "53842"
values.ExtUrl.Value = "http://127.0.0.1:53842/"
@@ -596,6 +598,7 @@ func createInputHeaderAuth() setupValues {
values.init()
values.BindLocalhost.Value = "0"
values.PublicName.Value = "Test Name"
values.UseSsl.Value = "1"
values.Port.Value = "53842"
values.ExtUrl.Value = "http://127.0.0.1:53842/"

View File

@@ -106,6 +106,11 @@
<div class="wizard-input-section">
<div class="form-group">
<div class="col-sm-8">
<label for="public_name">Pubblic Name:</label>
<input type="text" class="form-control" id="public_name" name="public_name" value="Gokapi" placeholder="Public Name" required>
</div><br><br><br>
<div class="col-sm-8">
<label for="port">Port:</label>
{{ if .IsDocker }}
@@ -122,6 +127,7 @@
<input type="text" class="form-control" id="url" name="url" value="http://127.0.0.1:53842/" onfocusout="extUrlChanged(this)" placeholder="Public URL" data-min="8" required data-validate="validateUrl">
</div><br><br><br>
<div class="col-sm-8">
<label for="url_redirection">Redirection URL for the index:</label>
@@ -616,6 +622,7 @@ function TestAWS(button) {
{{ end }}
document.getElementById("port").value = "{{ .Port }}";
document.getElementById("url").value = "{{ .Settings.ServerUrl }}";
document.getElementById("public_name").value = "{{ .Settings.PublicName }}";
document.getElementById("url_redirection").value = "{{ .Settings.RedirectUrl }}";
document.getElementById("authentication_sel").value = "{{ .Auth.Method }}";
authSelectionChanged("{{ .Auth.Method }}")

View File

@@ -11,6 +11,7 @@ type Configuration struct {
Port string `json:"Port"`
ServerUrl string `json:"ServerUrl"`
RedirectUrl string `json:"RedirectUrl"`
PublicName string `json:"PublicName"`
ConfigVersion int `json:"ConfigVersion"`
LengthId int `json:"LengthId"`
DataDir string `json:"DataDir"`

View File

@@ -23,12 +23,13 @@ var testConfig = Configuration{
Port: ":12345",
ServerUrl: "https://testserver.com/",
RedirectUrl: "https://test.com",
ConfigVersion: 11,
ConfigVersion: 14,
LengthId: 5,
DataDir: "test",
MaxMemory: 50,
UseSsl: true,
MaxFileSizeMB: 20,
PublicName: "public-name",
Encryption: Encryption{
Level: 1,
Cipher: []byte{0x00},
@@ -47,4 +48,4 @@ func TestConfiguration_ToString(t *testing.T) {
test.IsEqualString(t, testConfig.ToString(), exptectedUnidentedOutput)
}
const exptectedUnidentedOutput = `{"Authentication":{"Method":0,"SaltAdmin":"saltadmin","SaltFiles":"saltfiles","Username":"admin","Password":"adminpwhashed","HeaderKey":"","OauthProvider":"","OAuthClientId":"","OAuthClientSecret":"","HeaderUsers":null,"OauthUsers":null},"Port":":12345","ServerUrl":"https://testserver.com/","RedirectUrl":"https://test.com","ConfigVersion":11,"LengthId":5,"DataDir":"test","MaxMemory":50,"UseSsl":true,"MaxFileSizeMB":20,"Encryption":{"Level":1,"Cipher":"AA==","Salt":"encsalt","Checksum":"encsum","ChecksumSalt":"encsumsalt"},"PicturesAlwaysLocal":true}`
const exptectedUnidentedOutput = `{"Authentication":{"Method":0,"SaltAdmin":"saltadmin","SaltFiles":"saltfiles","Username":"admin","Password":"adminpwhashed","HeaderKey":"","OauthProvider":"","OAuthClientId":"","OAuthClientSecret":"","HeaderUsers":null,"OauthUsers":null},"Port":":12345","ServerUrl":"https://testserver.com/","RedirectUrl":"https://test.com","PublicName":"public-name","ConfigVersion":14,"LengthId":5,"DataDir":"test","MaxMemory":50,"UseSsl":true,"MaxFileSizeMB":20,"Encryption":{"Level":1,"Cipher":"AA==","Salt":"encsalt","Checksum":"encsum","ChecksumSalt":"encsumsalt"},"PicturesAlwaysLocal":true}`

View File

@@ -5,6 +5,7 @@ Handling of webserver and requests / uploads
*/
import (
"bytes"
"context"
"embed"
"encoding/base64"
@@ -35,6 +36,7 @@ import (
"os"
"sort"
"strings"
templatetext "text/template"
"time"
)
@@ -71,8 +73,6 @@ var templateFolder *template.Template
var imageExpiredPicture []byte
const expiredFile = "static/expired.png"
var srv http.Server
var sseServer *sse.Server
@@ -90,13 +90,12 @@ func Start() {
if helper.FolderExists("static") {
fmt.Println("Found folder 'static', using local folder instead of internal static folder")
mux.Handle("/", http.FileServer(http.Dir("static")))
imageExpiredPicture, err = os.ReadFile(expiredFile)
helper.Check(err)
} else {
mux.Handle("/", http.FileServer(http.FS(webserverDir)))
imageExpiredPicture, err = fs.ReadFile(staticFolderEmbedded, "web/"+expiredFile)
helper.Check(err)
}
loadExpiryImage()
mux.HandleFunc("/admin", requireLogin(showAdminMenu, false))
mux.HandleFunc("/api/", processApi)
mux.HandleFunc("/apiDelete", requireLogin(deleteApiKey, false))
@@ -157,6 +156,16 @@ func Start() {
}
}
func loadExpiryImage() {
svgTemplate, err := templatetext.ParseFS(templateFolderEmbedded, "web/templates/expired_file_svg.tmpl")
helper.Check(err)
var buf bytes.Buffer
view := UploadView{}
err = svgTemplate.Execute(&buf, view.convertGlobalConfig(ViewMain))
helper.Check(err)
imageExpiredPicture = buf.Bytes()
}
// Shutdown closes the webserver gracefully
func Shutdown() {
sseServer.Close()
@@ -211,7 +220,7 @@ func doLogout(w http.ResponseWriter, r *http.Request) {
// Handling of /index and redirecting to globalConfig.RedirectUrl
func showIndex(w http.ResponseWriter, r *http.Request) {
err := templateFolder.ExecuteTemplate(w, "index", genericView{RedirectUrl: configuration.Get().RedirectUrl})
err := templateFolder.ExecuteTemplate(w, "index", genericView{RedirectUrl: configuration.Get().RedirectUrl, PublicName: configuration.Get().PublicName})
helper.Check(err)
}
@@ -228,19 +237,19 @@ func showError(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Has("key") {
errorReason = wrongCipher
}
err := templateFolder.ExecuteTemplate(w, "error", genericView{ErrorId: errorReason})
err := templateFolder.ExecuteTemplate(w, "error", genericView{ErrorId: errorReason, PublicName: configuration.Get().PublicName})
helper.Check(err)
}
// Handling of /error-auth
func showErrorAuth(w http.ResponseWriter, r *http.Request) {
err := templateFolder.ExecuteTemplate(w, "error_auth", genericView{})
err := templateFolder.ExecuteTemplate(w, "error_auth", genericView{PublicName: configuration.Get().PublicName})
helper.Check(err)
}
// Handling of /forgotpw
func forgotPassword(w http.ResponseWriter, r *http.Request) {
err := templateFolder.ExecuteTemplate(w, "forgotpw", genericView{})
err := templateFolder.ExecuteTemplate(w, "forgotpw", genericView{PublicName: configuration.Get().PublicName})
helper.Check(err)
}
@@ -303,15 +312,18 @@ func showLogin(w http.ResponseWriter, r *http.Request) {
IsFailedLogin: failedLogin,
User: user,
IsAdminView: false,
PublicName: configuration.Get().PublicName,
})
helper.Check(err)
}
// LoginView contains variables for the login template
type LoginView struct {
IsFailedLogin bool
User string
IsAdminView bool
IsFailedLogin bool
IsAdminView bool
IsDownloadView bool
User string
PublicName string
}
// Handling of /d
@@ -330,7 +342,9 @@ func showDownload(w http.ResponseWriter, r *http.Request) {
Name: file.Name,
Size: file.Size,
Id: file.Id,
IsDownloadView: true,
EndToEndEncryption: file.Encryption.IsEndToEndEncrypted,
PublicName: configuration.Get().PublicName,
IsFailedLogin: false,
UsesHttps: configuration.UsesHttps(),
}
@@ -354,6 +368,7 @@ func showDownload(w http.ResponseWriter, r *http.Request) {
case <-time.After(1 * time.Second):
}
}
view.IsPasswordView = true
err := templateFolder.ExecuteTemplate(w, "download_password", view)
helper.Check(err)
return
@@ -376,7 +391,7 @@ func showHotlink(w http.ResponseWriter, r *http.Request) {
hotlinkId := strings.Replace(r.URL.Path, "/hotlink/", "", 1)
file, ok := storage.GetFileByHotlink(hotlinkId)
if !ok {
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Content-Type", "image/svg+xml")
_, err := w.Write(imageExpiredPicture)
helper.Check(err)
return
@@ -487,7 +502,7 @@ func showE2ESetup(w http.ResponseWriter, r *http.Request) {
return
}
e2einfo := database.GetEnd2EndInfo()
err := templateFolder.ExecuteTemplate(w, "e2esetup", e2ESetupView{HasBeenSetup: e2einfo.HasBeenSetUp()})
err := templateFolder.ExecuteTemplate(w, "e2esetup", e2ESetupView{HasBeenSetup: e2einfo.HasBeenSetUp(), PublicName: configuration.Get().PublicName})
helper.Check(err)
}
@@ -496,17 +511,22 @@ type DownloadView struct {
Name string
Size string
Id string
Cipher string
PublicName string
IsFailedLogin bool
IsAdminView bool
IsDownloadView bool
IsPasswordView bool
ClientSideDecryption bool
EndToEndEncryption bool
UsesHttps bool
Cipher string
}
type e2ESetupView struct {
IsAdminView bool
HasBeenSetup bool
IsAdminView bool
IsDownloadView bool
HasBeenSetup bool
PublicName string
}
// UploadView contains parameters for the admin menu template
@@ -516,25 +536,27 @@ type UploadView struct {
Url string
HotlinkUrl string
GenericHotlinkUrl string
TimeNow int64
IsAdminView bool
IsApiView bool
MaxFileSize int
IsLogoutAvailable bool
DefaultDownloads int
DefaultExpiry int
DefaultPassword string
Logs string
PublicName string
IsAdminView bool
IsDownloadView bool
IsApiView bool
IsLogoutAvailable bool
DefaultUnlimitedDownload bool
DefaultUnlimitedTime bool
EndToEndEncryption bool
MaxFileSize int
DefaultDownloads int
DefaultExpiry int
ActiveView int
Logs string
TimeNow int64
}
// ViewMain is the identifier for the main menu
const ViewMain = 0
// ViewLogs is the identifier for the log viever menu
// ViewLogs is the identifier for the log viewer menu
const ViewLogs = 1
// ViewAPI is the identifier for the API menu
@@ -583,15 +605,18 @@ func (u *UploadView) convertGlobalConfig(view int) *UploadView {
}
}
u.Url = configuration.Get().ServerUrl + "d?id="
u.HotlinkUrl = configuration.Get().ServerUrl + "hotlink/"
u.GenericHotlinkUrl = configuration.Get().ServerUrl + "downloadFile?id="
config := configuration.Get()
u.Url = config.ServerUrl + "d?id="
u.HotlinkUrl = config.ServerUrl + "hotlink/"
u.GenericHotlinkUrl = config.ServerUrl + "downloadFile?id="
u.Items = result
u.PublicName = config.PublicName
u.ApiKeys = resultApi
u.TimeNow = time.Now().Unix()
u.IsAdminView = true
u.ActiveView = view
u.MaxFileSize = configuration.Get().MaxFileSizeMB
u.MaxFileSize = config.MaxFileSizeMB
u.IsLogoutAvailable = authentication.IsLogoutAvailable()
defaultValues := database.GetUploadDefaults()
u.DefaultDownloads = defaultValues.Downloads
@@ -599,7 +624,7 @@ func (u *UploadView) convertGlobalConfig(view int) *UploadView {
u.DefaultPassword = defaultValues.Password
u.DefaultUnlimitedDownload = defaultValues.UnlimitedDownload
u.DefaultUnlimitedTime = defaultValues.UnlimitedTime
u.EndToEndEncryption = configuration.Get().Encryption.Level == encryption.EndToEndEncryption
u.EndToEndEncryption = config.Encryption.Level == encryption.EndToEndEncryption
return u
}
@@ -698,7 +723,9 @@ func addNoCacheHeader(w http.ResponseWriter) {
// A view containing parameters for a generic template
type genericView struct {
IsAdminView bool
RedirectUrl string
ErrorId int
IsAdminView bool
IsDownloadView bool
PublicName string
RedirectUrl string
ErrorId int
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/forceu/gokapi/internal/webserver/authentication"
"github.com/r3labs/sse/v2"
"html/template"
"io/fs"
"os"
"strings"
"testing"
@@ -35,13 +34,9 @@ func TestEmbedFs(t *testing.T) {
if err != nil {
t.Error("Unable to read templates")
}
if !strings.Contains(templates.DefinedTemplates(), "app_name") {
if !strings.Contains(templates.DefinedTemplates(), "header") {
t.Error("Unable to parse templates")
}
_, err = fs.Stat(staticFolderEmbedded, "web/static/expired.png")
if err != nil {
t.Error("Static webdir incomplete")
}
}
func TestIndexRedirect(t *testing.T) {
@@ -326,7 +321,7 @@ func TestDownloadHotlink(t *testing.T) {
// Download expired hotlink
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/hotlink/PhSs6mFtf8O5YGlLMfNw9rYXx9XRNkzCnJZpQBi7inunv3Z4A.jpg",
RequiredContent: []string{"Created with GIMP"},
RequiredContent: []string{"The requested image has expired"},
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -1,6 +1,6 @@
function parseHashValue(id) {
let key = localStorage.getItem("key-" + id);
let filename = localStorage.getItem("fn-" + id);
let key = sessionStorage.getItem("key-" + id);
let filename = sessionStorage.getItem("fn-" + id);
if (key === null || filename === null) {
hash = window.location.hash.substr(1);
@@ -20,8 +20,8 @@ function parseHashValue(id) {
redirectToE2EError();
return;
}
localStorage.setItem("key-" + id, info.c);
localStorage.setItem("fn-" + id, info.f);
sessionStorage.setItem("key-" + id, info.c);
sessionStorage.setItem("fn-" + id, info.f);
}
}

View File

@@ -1 +1 @@
function parseHashValue(e){let t=localStorage.getItem("key-"+e),n=localStorage.getItem("fn-"+e);if(t===null||n===null){if(hash=window.location.hash.substr(1),hash.length<50){redirectToE2EError();return}let t;try{let e=atob(hash);t=JSON.parse(e)}catch{redirectToE2EError();return}if(!isCorrectJson(t)){redirectToE2EError();return}localStorage.setItem("key-"+e,t.c),localStorage.setItem("fn-"+e,t.f)}}function isCorrectJson(e){return e.f!==void 0&&e.c!==void 0&&typeof e.f=="string"&&typeof e.c=="string"&&e.f!=""&&e.c!=""}function redirectToE2EError(){window.location="./error?e2e"}
function parseHashValue(e){let t=sessionStorage.getItem("key-"+e),n=sessionStorage.getItem("fn-"+e);if(t===null||n===null){if(hash=window.location.hash.substr(1),hash.length<50){redirectToE2EError();return}let t;try{let e=atob(hash);t=JSON.parse(e)}catch{redirectToE2EError();return}if(!isCorrectJson(t)){redirectToE2EError();return}sessionStorage.setItem("key-"+e,t.c),sessionStorage.setItem("fn-"+e,t.f)}}function isCorrectJson(e){return e.f!==void 0&&e.c!==void 0&&typeof e.f=="string"&&typeof e.c=="string"&&e.f!=""&&e.c!=""}function redirectToE2EError(){window.location="./error?e2e"}

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="300" viewBox="0 0 500 300">
<rect width="100%" height="100%" fill="#888888" />
<text x="50%" y="33%" fill="#ffffff" font-family="Arial" font-size="36" text-anchor="middle">{{.PublicName}}</text>
<text x="50%" y="65%" fill="#ffffff" font-family="Arial" font-size="24" text-anchor="middle">The requested image has expired</text>
</svg>

After

Width:  |  Height:  |  Size: 400 B

View File

@@ -78,10 +78,10 @@
async function DownloadEncrypted() {
try {
{{ if .EndToEndEncryption }}
let key = localStorage.getItem("key-{{ .Id }}");
localStorage.removeItem("key-{{ .Id }}");
let filename = localStorage.getItem("fn-{{ .Id }}");
localStorage.removeItem("fn-{{ .Id }}");
let key = sessionStorage.getItem("key-{{ .Id }}");
sessionStorage.removeItem("key-{{ .Id }}");
let filename = sessionStorage.getItem("fn-{{ .Id }}");
sessionStorage.removeItem("fn-{{ .Id }}");
{{ else }}
let key = "{{ .Cipher }}";
{{ end }}
@@ -141,7 +141,9 @@
{{ end }}
{{ if .EndToEndEncryption }}
<script>
document.getElementById("filename").innerText = localStorage.getItem("fn-{{ .Id }}");
let filename = sessionStorage.getItem("fn-{{ .Id }}");
document.getElementById("filename").innerText = filename;
document.title = "{{.PublicName}}: "+ filename;
</script>
{{ end }}

View File

@@ -13,7 +13,7 @@
<link rel="manifest" href="/site.webmanifest">
<link href="css/min/gokapi.min.css?v={{ template "css_main"}}" rel="stylesheet">
{{ if .IsAdminView }}
<title>{{template "app_name"}} Admin</title>
<title>{{.PublicName}} Admin</title>
<link href="./assets/dist/css/dropzone.min.css" rel="stylesheet">
<link href="./assets/dist/css/datatables.min.css" rel="stylesheet" />
<script src="./assets/dist/js/jquery.min.js"></script>
@@ -31,7 +31,15 @@
}
</style>
{{ else }}
<title>{{template "app_name"}}</title>
{{ if .IsDownloadView }}
{{ if .IsPasswordView }}
<title>{{.PublicName}}: Password required</title>
{{ else }}
<title>{{.PublicName}}: {{.Name}}</title>
{{end }}
{{ else }}
<title>{{.PublicName}}</title>
{{end }}
<style>
body {
box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5);
@@ -45,7 +53,7 @@
<div class="d-flex w-100 h-100 p-3 mx-auto flex-column">
<header class="masthead mb-auto">
<div style="max-width: 80em; margin: 0 auto;" class="inner">
<h1>{{template "app_name"}}</h1>
<h1>{{.PublicName}}</h1>
<nav class="nav nav-masthead justify-content-center">
<a class="nav-link {{ if eq .ActiveView 0}}active{{ end }}" href="./admin">Upload</a>
<a class="nav-link {{ if eq .ActiveView 1 }}active{{ end }}" href="./logs">Logs</a>
@@ -59,7 +67,7 @@
<div class="d-flex w-100 h-100 p-3 mx-auto flex-column">
<header class="mb-auto">
<div>
<h1><a href="/index" style="text-decoration: none;display: block;">{{template "app_name"}}</a></h1>
<h1><a href="/index" style="text-decoration: none;display: block;">{{.PublicName}}</a></h1>
</div>
</header>
<main>

View File

@@ -1,10 +1,9 @@
// Change these for rebranding
{{define "app_name"}}Gokapi{{end}}
{{define "version"}}1.7.1{{end}}
{{define "version"}}1.7.2{{end}}
// Specifies the version of JS files, so that the browser doesn't
// use a cached version, if the file has been updated
{{define "js_admin_version"}}1{{end}}
{{define "js_dropzone_version"}}3{{end}}
{{define "js_e2eversion"}}2{{end}}
{{define "css_main"}}1{{end}}
{{define "css_main"}}1{{end}}