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`. - 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. - 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 ### Running on Docker
You can pull the official Docker Image from [Docker Hub](https://hub.docker.com/r/listmonk/listmonk). 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/jordan-wright/email v0.0.0-20200307200233-de844847de93
github.com/knadh/goyesql/v2 v2.1.1 github.com/knadh/goyesql/v2 v2.1.1
github.com/knadh/koanf v0.8.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/echo v3.3.10+incompatible
github.com/labstack/gommon v0.3.0 // indirect github.com/labstack/gommon v0.3.0 // indirect
github.com/lib/pq v1.3.0 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/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 h1:NQon6PTpLXies4bRFhS3VpLCf6y+jn6YVXU3i2wPQ+M=
github.com/knadh/stuffbin v1.0.0/go.mod h1:yVCFaWaKPubSNibBsTAJ939q2ABHudJQxRWZWV5yh+4= 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/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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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 // initFileSystem initializes the stuffbin FileSystem to provide
// access to bunded static assets to the app. // access to bunded static assets to the app.
func initFS() stuffbin.FileSystem { func initFS(staticDir string) stuffbin.FileSystem {
// Get the executable's path. // Get the executable's path.
path, err := os.Executable() path, err := os.Executable()
if err != nil { if err != nil {
log.Fatalf("error getting executable path: %v", err) log.Fatalf("error getting executable path: %v", err)
} }
// Load the static files stuffed in the binary.
fs, err := stuffbin.UnStuff(path) 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 { 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 return fs
} }

View File

@ -157,7 +157,7 @@ func newConfigFile() error {
// Initialize the static file system into which all // Initialize the static file system into which all
// required static assets (.sql, .js files etc.) are loaded. // required static assets (.sql, .js files etc.) are loaded.
fs := initFS() fs := initFS("")
b, err := fs.Read("config.toml.sample") b, err := fs.Read("config.toml.sample")
if err != nil { if err != nil {
return fmt.Errorf("error reading sample config (is binary stuffed?): %v", err) 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. // Register the commandline flags.
f.StringSlice("config", []string{"config.toml"}, f.StringSlice("config", []string{"config.toml"},
"Path to one or more config files (will be merged in order)") "path to one or more config files (will be merged in order)")
f.Bool("install", false, "Run first time installation") f.Bool("install", false, "run first time installation")
f.Bool("version", false, "Current version of the build") f.Bool("version", false, "current version of the build")
f.Bool("new-config", false, "Generate sample config file") f.Bool("new-config", false, "generate sample config file")
f.Bool("yes", false, "Assume 'yes' to prompts, eg: during --install") 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 { if err := f.Parse(os.Args[1:]); err != nil {
lo.Fatalf("error loading flags: %v", err) 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 // Initialize the DB and the filesystem that are required by the installer
// and the app. // and the app.
var ( var (
fs = initFS() fs = initFS(ko.String("static-dir"))
db = initDB() db = initDB()
) )
defer db.Close() 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 }}