Add support for loading custom static files with `--static-dir`
- Removed duplicate copies of static files in `static/public/`
This commit is contained in:
parent
bbe239ba29
commit
71803ab1af
|
@ -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
2
go.mod
|
@ -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
2
go.sum
|
@ -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
66
init.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
13
main.go
|
@ -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()
|
||||||
|
|
|
@ -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"> </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"> </div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
{{ end }}
|
|
|
@ -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 }}
|
|
|
@ -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;"> </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;"> {{ TrackView }}</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -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 }}
|
|
|
@ -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 }}
|
|
|
@ -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 }}
|
|
|
@ -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 }}
|
|
Loading…
Reference in New Issue