Add support for loading custom static files with `--static-dir`

- Removed duplicate copies of static files in `static/public/`
This commit is contained in:
Kailash Nadh 2020-03-14 21:07:14 +05:30
parent bbe239ba29
commit 71803ab1af
13 changed files with 55 additions and 306 deletions

View File

@ -14,6 +14,9 @@ listmonk is a standalone, self-hosted, newsletter and mailing list manager. It i
- Run `./listmonk` and visit `http://localhost:9000`.
- Since there is no user auth yet, it's best to put listmonk behind a proxy like Nginx and setup basicauth on all endpoints except for the few endpoints that need to be public. Here is a [sample nginx config](https://github.com/knadh/listmonk/wiki/Production-Nginx-config) for production use.
### Configuration and customization
See the [configuration Wiki page](https://github.com/knadh/listmonk/wiki/Configuration).
### Running on Docker
You can pull the official Docker Image from [Docker Hub](https://hub.docker.com/r/listmonk/listmonk).

2
go.mod
View File

@ -7,7 +7,7 @@ require (
github.com/jordan-wright/email v0.0.0-20200307200233-de844847de93
github.com/knadh/goyesql/v2 v2.1.1
github.com/knadh/koanf v0.8.1
github.com/knadh/stuffbin v1.0.0
github.com/knadh/stuffbin v1.1.0
github.com/labstack/echo v3.3.10+incompatible
github.com/labstack/gommon v0.3.0 // indirect
github.com/lib/pq v1.3.0

2
go.sum
View File

@ -29,6 +29,8 @@ github.com/knadh/koanf v0.8.1 h1:4VLACWqrkWRQIup3ooq6lOnaSbOJSNO+YVXnJn/NPZ8=
github.com/knadh/koanf v0.8.1/go.mod h1:kVvmDbXnBtW49Czi4c1M+nnOWF0YSNZ8BaKvE/bCO1w=
github.com/knadh/stuffbin v1.0.0 h1:NQon6PTpLXies4bRFhS3VpLCf6y+jn6YVXU3i2wPQ+M=
github.com/knadh/stuffbin v1.0.0/go.mod h1:yVCFaWaKPubSNibBsTAJ939q2ABHudJQxRWZWV5yh+4=
github.com/knadh/stuffbin v1.1.0 h1:f5S5BHzZALjuJEgTIOMC9NidEnBJM7Ze6Lu1GHR/lwU=
github.com/knadh/stuffbin v1.1.0/go.mod h1:yVCFaWaKPubSNibBsTAJ939q2ABHudJQxRWZWV5yh+4=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=

66
init.go
View File

@ -29,41 +29,57 @@ const (
// initFileSystem initializes the stuffbin FileSystem to provide
// access to bunded static assets to the app.
func initFS() stuffbin.FileSystem {
func initFS(staticDir string) stuffbin.FileSystem {
// Get the executable's path.
path, err := os.Executable()
if err != nil {
log.Fatalf("error getting executable path: %v", err)
}
// Load the static files stuffed in the binary.
fs, err := stuffbin.UnStuff(path)
if err == nil {
return fs
}
// Running in local mode. Load the required static assets into
// the in-memory stuffbin.FileSystem.
lo.Printf("unable to initialize embedded filesystem: %v", err)
lo.Printf("using local filesystem for static assets")
files := []string{
"config.toml.sample",
"queries.sql",
"schema.sql",
"static/email-templates",
// Alias /static/public to /public for the HTTP fileserver.
"static/public:/public",
// The frontend app's static assets are aliased to /frontend
// so that they are accessible at localhost:port/frontend/static/ ...
"frontend/build:/frontend",
}
fs, err = stuffbin.NewLocalFS("/", files...)
if err != nil {
lo.Fatalf("failed to initialize local file for assets: %v", err)
// Running in local mode. Load local assets into
// the in-memory stuffbin.FileSystem.
lo.Printf("unable to initialize embedded filesystem: %v", err)
lo.Printf("using local filesystem for static assets")
files := []string{
"config.toml.sample",
"queries.sql",
"schema.sql",
"static/email-templates",
// Alias /static/public to /public for the HTTP fileserver.
"static/public:/public",
// The frontend app's static assets are aliased to /frontend
// so that they are accessible at localhost:port/frontend/static/ ...
"frontend/build:/frontend",
}
fs, err = stuffbin.NewLocalFS("/", files...)
if err != nil {
lo.Fatalf("failed to initialize local file for assets: %v", err)
}
}
// Optional static directory to override files.
if staticDir != "" {
lo.Printf("loading static files from: %v", staticDir)
fStatic, err := stuffbin.NewLocalFS("/", []string{
filepath.Join(staticDir, "/email-templates") + ":/static/email-templates",
// Alias /static/public to /public for the HTTP fileserver.
filepath.Join(staticDir, "/public") + ":/public",
}...)
if err != nil {
lo.Fatalf("failed reading static directory: %s: %v", staticDir, err)
}
if err := fs.Merge(fStatic); err != nil {
lo.Fatalf("error merging static directory: %s: %v", staticDir, err)
}
}
return fs
}

View File

@ -157,7 +157,7 @@ func newConfigFile() error {
// Initialize the static file system into which all
// required static assets (.sql, .js files etc.) are loaded.
fs := initFS()
fs := initFS("")
b, err := fs.Read("config.toml.sample")
if err != nil {
return fmt.Errorf("error reading sample config (is binary stuffed?): %v", err)

13
main.go
View File

@ -57,11 +57,12 @@ func init() {
// Register the commandline flags.
f.StringSlice("config", []string{"config.toml"},
"Path to one or more config files (will be merged in order)")
f.Bool("install", false, "Run first time installation")
f.Bool("version", false, "Current version of the build")
f.Bool("new-config", false, "Generate sample config file")
f.Bool("yes", false, "Assume 'yes' to prompts, eg: during --install")
"path to one or more config files (will be merged in order)")
f.Bool("install", false, "run first time installation")
f.Bool("version", false, "current version of the build")
f.Bool("new-config", false, "generate sample config file")
f.String("static-dir", "", "(optional) path to directory with static files")
f.Bool("yes", false, "assume 'yes' to prompts, eg: during --install")
if err := f.Parse(os.Args[1:]); err != nil {
lo.Fatalf("error loading flags: %v", err)
@ -111,7 +112,7 @@ func main() {
// Initialize the DB and the filesystem that are required by the installer
// and the app.
var (
fs = initFS()
fs = initFS(ko.String("static-dir"))
db = initDB()
)
defer db.Close()

View File

@ -1,98 +0,0 @@
{{ define "header" }}
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<base target="_blank">
<style>
body {
background-color: #F0F1F3;
font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, sans-serif;
font-size: 15px;
line-height: 26px;
margin: 0;
color: #444;
}
.wrap {
background-color: #fff;
padding: 30px;
max-width: 525px;
margin: 0 auto;
border-radius: 5px;
}
.header {
border-bottom: 1px solid #eee;
padding-bottom: 15px;
margin-bottom: 15px;
}
.footer {
text-align: center;
font-size: 12px;
color: #888;
}
.footer a {
color: #888;
}
.gutter {
padding: 30px;
}
.button {
background: #7f2aff;
color: #fff !important;
display: inline-block;
border-radius: 3px;
padding: 10px 30px;
text-align: center;
text-decoration: none;
font-weight: bold;
}
.button:hover {
background: #222;
color: #fff;
}
img {
max-width: 100%;
}
a {
color: #7f2aff;
}
a:hover {
color: #111;
}
@media screen and (max-width: 600px) {
.wrap {
max-width: auto;
}
.gutter {
padding: 10px;
}
}
</style>
</head>
<body style="background-color: #F0F1F3;">
<div class="gutter">&nbsp;</div>
<div class="wrap">
<div class="header">
{{ if ne LogoURL "" }}
<a href="{{ RootURL }}"><img src="{{ LogoURL }}" alt="listmonk" /></a>
{{ end }}
</div>
{{ end }}
{{ define "footer" }}
</div>
<div class="footer">
<p>Powered by <a href="https://listmonk.app" target="_blank">listmonk</a></p>
</div>
<div class="gutter">&nbsp;</div>
</body>
</html>
{{ end }}

View File

@ -1,25 +0,0 @@
{{ define "campaign-status" }}
{{ template "header" . }}
<h2>Campaign update</h2>
<table width="100%">
<tr>
<td width="30%"><strong>Campaign</strong></td>
<td><a href="{{ index . "RootURL" }}/campaigns/{{ index . "ID" }}">{{ index . "Name" }}</a></td>
</tr>
<tr>
<td width="30%"><strong>Status</strong></td>
<td>{{ index . "Status" }}</td>
</tr>
<tr>
<td width="30%"><strong>Sent</strong></td>
<td>{{ index . "Sent" }} / {{ index . "ToSend" }}</td>
</tr>
{{ if ne (index . "Reason") "" }}
<tr>
<td width="30%"><strong>Reason</strong></td>
<td>{{ index . "Reason" }}</td>
</tr>
{{ end }}
</table>
{{ template "footer" }}
{{ end }}

View File

@ -1,84 +0,0 @@
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<base target="_blank">
<style>
body {
background-color: #F0F1F3;
font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, sans-serif;
font-size: 15px;
line-height: 26px;
margin: 0;
color: #444;
}
.wrap {
background-color: #fff;
padding: 30px;
max-width: 525px;
margin: 0 auto;
border-radius: 5px;
}
.button {
background: #7f2aff;
border-radius: 3px;
text-decoration: none !important;
color: #fff !important;
font-weight: bold;
padding: 10px 30px;
display: inline-block;
}
.button:hover {
background: #111;
}
.footer {
text-align: center;
font-size: 12px;
color: #888;
}
.footer a {
color: #888;
}
.gutter {
padding: 30px;
}
img {
max-width: 100%;
}
a {
color: #7f2aff;
}
a:hover {
color: #111;
}
@media screen and (max-width: 600px) {
.wrap {
max-width: auto;
}
.gutter {
padding: 10px;
}
}
</style>
</head>
<body style="background-color: #F0F1F3;font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, sans-serif;font-size: 15px;line-height: 26px;margin: 0;color: #444;">
<div class="gutter" style="padding: 30px;">&nbsp;</div>
<div class="wrap" style="background-color: #fff;padding: 30px;max-width: 525px;margin: 0 auto;border-radius: 5px;">
{{ template "content" . }}
</div>
<div class="footer" style="text-align: center;font-size: 12px;color: #888;">
<p>Don't want to receive these e-mails? <a href="{{ UnsubscribeURL }}" style="color: #888;">Unsubscribe</a></p>
<p>Powered by <a href="https://listmonk.app" target="_blank" style="color: #888;">listmonk</a></p>
</div>
<div class="gutter" style="padding: 30px;">&nbsp;{{ TrackView }}</div>
</body>
</html>

View File

@ -1,19 +0,0 @@
{{ define "import-status" }}
{{ template "header" . }}
<h2>Import update</h2>
<table width="100%">
<tr>
<td width="30%"><strong>File</strong></td>
<td><a href="{{ RootURL }}/subscribers/import">{{ .Name }}</a></td>
</tr>
<tr>
<td width="30%"><strong>Status</strong></td>
<td>{{ .Status }}</td>
</tr>
<tr>
<td width="30%"><strong>Records</strong></td>
<td>{{ .Imported }} / {{ .Total }}</td>
</tr>
</table>
{{ template "footer" }}
{{ end }}

View File

@ -1,9 +0,0 @@
{{ define "subscriber-data" }}
{{ template "header" . }}
<h2>Your data</h2>
<p>
A copy of all data recorded on you is attached as a file in JSON format.
It can be viewed in a text editor.
</p>
{{ template "footer" }}
{{ end }}

View File

@ -1,17 +0,0 @@
{{ define "optin-campaign" }}
<p>Hi {{`{{ .Subscriber.FirstName }}`}},</p>
<p>You have been added to the following mailing lists:</p>
<ul>
{{ range $i, $l := .Lists }}
{{ if eq .Type "public" }}
<li>{{ .Name }}</li>
{{ else }}
<li>Private list</li>
{{ end }}
{{ end }}
</ul>
<p>
<a class="button" {{ .OptinURLAttr }} class="button">Confirm subscription(s)</a>
</p>
{{ end }}

View File

@ -1,21 +0,0 @@
{{ define "subscriber-optin" }}
{{ template "header" . }}
<h2>Confirm subscription</h2>
<p>Hi {{ .Subscriber.FirstName }},</p>
<p>You have been added to the following mailing lists:</p>
<ul>
{{ range $i, $l := .Lists }}
{{ if eq .Type "public" }}
<li>{{ .Name }}</li>
{{ else }}
<li>Private list</li>
{{ end }}
{{ end }}
</ul>
<p>Confirm your subscription by clicking the below button.</p>
<p>
<a href="{{ .OptinURL }}" class="button">Confirm subscription</a>
</p>
{{ template "footer" }}
{{ end }}