From a6a2d59ed96cd36e60eb4748e00de66c45b45698 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Sat, 4 Jan 2020 16:31:19 -0700
Subject: [PATCH] Compile assets into the binary

Fixes https://github.com/turt2live/matrix-media-repo/issues/214
---
 .gitignore                 |  3 ++
 CHANGELOG.md               |  4 ++
 README.md                  |  8 ++--
 build.ps1                  |  2 +
 build.sh                   |  2 +
 cmd/compile_assets/main.go | 78 +++++++++++++++++++++++++++++++++++++
 cmd/import_synapse/main.go |  6 ++-
 cmd/media_repo/main.go     | 11 ++++--
 common/assets/process.go   | 79 ++++++++++++++++++++++++++++++++++++++
 common/config/access.go    |  3 ++
 10 files changed, 188 insertions(+), 8 deletions(-)
 create mode 100644 cmd/compile_assets/main.go
 create mode 100644 common/assets/process.go

diff --git a/.gitignore b/.gitignore
index ecdfdf7b..17100f70 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,9 @@
 /vendor
 /config
 
+# Generated files
+assets.bin.go
+
 media-repo*.yaml
 homeserver.yaml
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cf0e4d40..bc2c80af 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 
 ## [Unreleased]
 
+### Added
+
+* Compile assets (templates and migrations) into the binary for ease of deployment.
+
 ### Fixed
 
 * Fix error message when an invalid access token is provided.
diff --git a/README.md b/README.md
index 551482b4..c7d209dc 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,9 @@ for your environment.
 
 For help and support, visit [#mediarepo:t2bot.io](https://matrix.to/#/#mediarepo:t2bot.io).
 
-# Installing / building
+## Installing / building
+
+**Note**: developing on matrix-media-repo requires you to follow these steps at least once.
 
 Assuming Go 1.12+ is already installed on your PATH:
 ```bash
@@ -46,7 +48,7 @@ docker run --rm -it -p 8000:8000 -v /etc/matrix-media-repo:/data turt2live/matri
 Note that using `latest` is dangerous - it is effectively the development branch for this project. Instead,
 prefer to use one of the tagged versions and update regularly.
 
-# Deployment
+## Deployment
 
 This is intended to run behind a load balancer and beside your homeserver deployments. Assuming your load balancer handles SSL termination, a sample nginx config would be:
 
@@ -97,7 +99,7 @@ listeners:
 
 After importing your media, setting `enable_media_repo: false` in your Synapse configuration will disable the media repository.
 
-# Importing media from synapse
+## Importing media from synapse
 
 Media is imported by connecting to your synapse database and downloading all the content from the homeserver. This is so 
 you have a backup of the media repository still with synapse. **Do not point traffic at the media repo until after the 
diff --git a/build.ps1 b/build.ps1
index be245b52..911ffc0a 100644
--- a/build.ps1
+++ b/build.ps1
@@ -1,3 +1,5 @@
 $GitCommit = (git rev-list -1 HEAD)
 $Version = (git describe --tags)
+go install -v ./cmd/compile_assets
+compile_assets
 go install -ldflags "-X github.com/turt2live/matrix-media-repo/common/version.GitCommit=$GitCommit -X github.com/turt2live/matrix-media-repo/common/version.Version=$Version" -v ./cmd/...
diff --git a/build.sh b/build.sh
index 15d83fd7..bfb787f4 100755
--- a/build.sh
+++ b/build.sh
@@ -1,3 +1,5 @@
 #!/bin/sh
 
+GOBIN=$PWD/bin go install -v ./cmd/compile_assets
+compile_assets
 GOBIN=$PWD/bin go install -ldflags "-X github.com/turt2live/matrix-media-repo/common/version.GitCommit=$(git rev-list -1 HEAD) -X github.com/turt2live/matrix-media-repo/common/version.Version=$(git describe --tags)" -v ./cmd/...
diff --git a/cmd/compile_assets/main.go b/cmd/compile_assets/main.go
new file mode 100644
index 00000000..bd654b0f
--- /dev/null
+++ b/cmd/compile_assets/main.go
@@ -0,0 +1,78 @@
+package main
+
+import (
+	"bytes"
+	"compress/gzip"
+	"encoding/hex"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"path"
+
+	"github.com/turt2live/matrix-media-repo/common/config"
+)
+
+func main() {
+	migrationsPath := flag.String("migrations", config.DefaultMigrationsPath, "The absolute path for the migrations folder")
+	templatesPath := flag.String("templates", config.DefaultTemplatesPath, "The absolute path for the templates folder")
+	outputFile := flag.String("output", "./common/assets/assets.bin.go", "The output Go file to dump the files into")
+	flag.Parse()
+
+	fmt.Println("Reading migrations into memory...")
+	migrations := readDir(*migrationsPath, "migrations")
+	templates := readDir(*templatesPath, "templates")
+
+	fileMap := make(map[string][]byte)
+	for k, v := range migrations {
+		fileMap[k] = v
+	}
+	for k, v := range templates {
+		fileMap[k] = v
+	}
+
+	fmt.Println("Writing assets go file")
+	str := "package " + path.Base(path.Dir(*outputFile)) + "\n\n"
+	str += "// !! THIS FILE IS AUTOMATICALLY GENERATED. You can edit it, but it will be overwritten over time.\n\n"
+	str += "var compressedFiles = map[string]string{\n"
+	for f, b := range fileMap {
+		b64 := hex.EncodeToString(b)
+		str += fmt.Sprintf("\t\"%s\": \"%s\",\n", f, b64)
+	}
+	str += "}\n"
+	err := ioutil.WriteFile(*outputFile, []byte(str), 644)
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println("Done")
+}
+
+func readDir(dir string, pathName string) map[string][]byte {
+	fileMap := make(map[string][]byte)
+	files, err := ioutil.ReadDir(dir)
+	if err != nil {
+		panic(err)
+	}
+	for _, f := range files {
+		fname := path.Join(dir, f.Name())
+		fmt.Println("Reading ", fname)
+
+		b, err := ioutil.ReadFile(fname)
+		if err != nil {
+			panic(err)
+		}
+
+		// Compress the file
+		fmt.Println("Compressing ", fname)
+		out := &bytes.Buffer{}
+		gw, err := gzip.NewWriterLevel(out, gzip.BestCompression)
+		if err != nil {
+			panic(err)
+		}
+		gw.Write(b)
+		gw.Close()
+
+		fileMap[path.Join(pathName, f.Name())] = out.Bytes()
+	}
+	return fileMap
+}
diff --git a/cmd/import_synapse/main.go b/cmd/import_synapse/main.go
index 651640cc..b1f6b77d 100644
--- a/cmd/import_synapse/main.go
+++ b/cmd/import_synapse/main.go
@@ -14,6 +14,7 @@ import (
 	"github.com/jeffail/tunny"
 	"github.com/sirupsen/logrus"
 	"github.com/turt2live/matrix-media-repo/common"
+	"github.com/turt2live/matrix-media-repo/common/assets"
 	"github.com/turt2live/matrix-media-repo/common/config"
 	"github.com/turt2live/matrix-media-repo/common/logging"
 	"github.com/turt2live/matrix-media-repo/common/rcontext"
@@ -43,7 +44,7 @@ func main() {
 	flag.Parse()
 
 	config.Path = *configPath
-	config.Runtime.MigrationsPath = *migrationsPath
+	assets.SetupTemplatesAndMigrations(*migrationsPath, "")
 
 	var realPsqlPassword string
 	if *postgresPassword == "" {
@@ -115,6 +116,9 @@ func main() {
 		time.Sleep(1 * time.Second)
 	}
 
+	// Clean up
+	assets.Cleanup()
+
 	logrus.Info("Import completed")
 }
 
diff --git a/cmd/media_repo/main.go b/cmd/media_repo/main.go
index 43207c35..d26fc9b6 100644
--- a/cmd/media_repo/main.go
+++ b/cmd/media_repo/main.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/sirupsen/logrus"
 	"github.com/turt2live/matrix-media-repo/api/webserver"
+	"github.com/turt2live/matrix-media-repo/common/assets"
 	"github.com/turt2live/matrix-media-repo/common/config"
 	"github.com/turt2live/matrix-media-repo/common/logging"
 	"github.com/turt2live/matrix-media-repo/common/runtime"
@@ -17,8 +18,8 @@ import (
 
 func main() {
 	configPath := flag.String("config", "media-repo.yaml", "The path to the configuration")
-	migrationsPath := flag.String("migrations", "./migrations", "The absolute path for the migrations folder")
-	templatesPath := flag.String("templates", "./templates", "The absolute path for the templates folder")
+	migrationsPath := flag.String("migrations", config.DefaultMigrationsPath, "The absolute path for the migrations folder")
+	templatesPath := flag.String("templates", config.DefaultTemplatesPath, "The absolute path for the templates folder")
 	versionFlag := flag.Bool("version", false, "Prints the version and exits")
 	flag.Parse()
 
@@ -34,8 +35,7 @@ func main() {
 	}
 
 	config.Path = *configPath
-	config.Runtime.MigrationsPath = *migrationsPath
-	config.Runtime.TemplatesPath = *templatesPath
+	assets.SetupTemplatesAndMigrations(*migrationsPath, *templatesPath)
 
 	err := logging.Setup(config.Get().General.LogDirectory)
 	if err != nil {
@@ -99,6 +99,9 @@ func main() {
 		stopAllButWeb()
 	}
 
+	// Clean up
+	assets.Cleanup()
+
 	// For debugging
 	logrus.Info("Goodbye!")
 }
diff --git a/common/assets/process.go b/common/assets/process.go
new file mode 100644
index 00000000..93a473f7
--- /dev/null
+++ b/common/assets/process.go
@@ -0,0 +1,79 @@
+package assets
+
+import (
+	"encoding/hex"
+	"io/ioutil"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+
+	"github.com/sirupsen/logrus"
+	"github.com/turt2live/matrix-media-repo/common/config"
+)
+
+var tempMigrations string
+var tempTemplates string
+
+func SetupTemplatesAndMigrations(givenMigrationsPath string, givenTemplatesPath string) {
+	_, err := os.Stat(givenMigrationsPath)
+	exists := err == nil || !os.IsNotExist(err)
+	if !exists {
+		tempMigrations, err = ioutil.TempDir(os.TempDir(), "media-repo-migrations")
+		if err != nil {
+			panic(err)
+		}
+		logrus.Info("Migrations path doesn't exist - attempting to unpack from compiled data")
+		extractPrefixTo("migrations", tempMigrations)
+		givenMigrationsPath = tempMigrations
+	}
+
+	if givenTemplatesPath != "" {
+		_, err = os.Stat(givenTemplatesPath)
+		exists = err == nil || !os.IsNotExist(err)
+		if !exists {
+			tempTemplates, err = ioutil.TempDir(os.TempDir(), "media-repo-templates")
+			if err != nil {
+				panic(err)
+			}
+			logrus.Info("Templates path doesn't exist - attempting to unpack from compiled data")
+			extractPrefixTo("templates", tempTemplates)
+			givenTemplatesPath = tempTemplates
+		}
+	}
+
+	config.Runtime.MigrationsPath = givenMigrationsPath
+	config.Runtime.TemplatesPath = givenTemplatesPath
+}
+
+func Cleanup() {
+	if tempMigrations != "" {
+		logrus.Info("Cleaning up temporary assets directory: ", tempMigrations)
+		os.Remove(tempMigrations)
+	}
+	if tempTemplates != "" {
+		logrus.Info("Cleaning up temporary assets directory: ", tempTemplates)
+		os.Remove(tempTemplates)
+	}
+}
+
+func extractPrefixTo(pathName string, destination string) {
+	for f, h := range compressedFiles {
+		if !strings.HasPrefix(f, pathName) {
+			continue
+		}
+
+		logrus.Infof("Decoding %s", f)
+		b, err := hex.DecodeString(h)
+		if err != nil {
+			panic(err)
+		}
+
+		dest := path.Join(destination, filepath.Base(f))
+		logrus.Infof("Writing %s to %s", f, dest)
+		err = ioutil.WriteFile(dest, b, 644)
+		if err != nil {
+			panic(err)
+		}
+	}
+}
diff --git a/common/config/access.go b/common/config/access.go
index 7f28be2c..69b6cbf2 100644
--- a/common/config/access.go
+++ b/common/config/access.go
@@ -17,6 +17,9 @@ type runtimeConfig struct {
 	TemplatesPath  string
 }
 
+const DefaultMigrationsPath = "./migrations"
+const DefaultTemplatesPath = "./templates"
+
 var Runtime = &runtimeConfig{}
 var Path = "media-repo.yaml"
 
-- 
GitLab