Initial support for subpath #39

This commit is contained in:
Radhi Fadlillah 2019-10-07 13:38:40 +07:00
parent 3077c7fbb8
commit 99d27930ea
13 changed files with 298 additions and 210 deletions

View File

@ -1,6 +1,8 @@
package cmd
import (
"strings"
"github.com/go-shiori/shiori/internal/webserver"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@ -18,15 +20,40 @@ func serveCmd() *cobra.Command {
cmd.Flags().IntP("port", "p", 8080, "Port used by the server")
cmd.Flags().StringP("address", "a", "", "Address the server listens to")
cmd.Flags().StringP("webroot", "r", "/", "Root path that used by server")
return cmd
}
func serveHandler(cmd *cobra.Command, args []string) {
// Get flags value
port, _ := cmd.Flags().GetInt("port")
address, _ := cmd.Flags().GetString("address")
rootPath, _ := cmd.Flags().GetString("webroot")
err := webserver.ServeApp(db, dataDir, address, port)
// Validate root path
if rootPath == "" {
rootPath = "/"
}
if !strings.HasPrefix(rootPath, "/") {
rootPath = "/" + rootPath
}
if !strings.HasSuffix(rootPath, "/") {
rootPath += "/"
}
// Start server
serverConfig := webserver.Config{
DB: db,
DataDir: dataDir,
ServerAddress: address,
ServerPort: port,
RootPath: rootPath,
}
err := webserver.ServeApp(serverConfig)
if err != nil {
logrus.Fatalf("Server error: %v\n", err)
}

View File

@ -2,52 +2,53 @@
<html lang="en">
<head>
<title>$$.Title$$ - Shiori - Bookmarks Manager</title>
<base href="$$.RootPath$$">
<title>$$.Book.Title$$ - Shiori - Bookmarks Manager</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="/res/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/res/apple-touch-icon-144x144.png">
<link rel="icon" type="image/png" href="/res/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="/res/favicon-16x16.png" sizes="16x16">
<link rel="icon" type="image/x-icon" href="/res/favicon.ico">
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="res/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="res/apple-touch-icon-144x144.png">
<link rel="icon" type="image/png" href="res/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="res/favicon-16x16.png" sizes="16x16">
<link rel="icon" type="image/x-icon" href="res/favicon.ico">
<link href="/css/source-sans-pro.min.css" rel="stylesheet">
<link href="/css/stylesheet.css" rel="stylesheet">
<link href="/css/custom-dialog.css" rel="stylesheet">
<link href="/css/bookmark-item.css" rel="stylesheet">
<link href="css/source-sans-pro.min.css" rel="stylesheet">
<link href="css/stylesheet.css" rel="stylesheet">
<link href="css/custom-dialog.css" rel="stylesheet">
<link href="css/bookmark-item.css" rel="stylesheet">
<script src="/js/dayjs.min.js"></script>
<script src="/js/vue.min.js"></script>
<script src="js/dayjs.min.js"></script>
<script src="js/vue.min.js"></script>
</head>
<body class="night">
<div id="content-scene" :class="{night: appOptions.nightMode}">
<div id="header">
<p id="metadata" v-cloak>Added {{localtime()}}</p>
<p id="title">$$.Title$$</p>
<p id="title">$$.Book.Title$$</p>
<div id="links">
<a href="$$.URL$$" target="_blank" rel="noopener">View Original</a>
$$if .HasArchive$$
<a href="/bookmark/$$.ID$$/archive">View Archive</a>
<a href="$$.Book.URL$$" target="_blank" rel="noopener">View Original</a>
$$if .Book.HasArchive$$
<a href="bookmark/$$.Book.ID$$/archive">View Archive</a>
$$end$$
</div>
</div>
<div id="content" v-pre>
$$html .HTML$$
$$html .Book.HTML$$
</div>
</div>
<script type="module">
// Create initial variable
import basePage from "/js/page/base.js";
import basePage from "./js/page/base.js";
new Vue({
el: '#content-scene',
mixins: [basePage],
data: {
modified: "$$.Modified$$"
modified: "$$.Book.Modified$$"
},
methods: {
localtime() {

View File

@ -2,25 +2,26 @@
<html lang="en">
<head>
<base href="$$.$$">
<title>Shiori - Bookmarks Manager</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="/res/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/res/apple-touch-icon-144x144.png">
<link rel="icon" type="image/png" href="/res/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="/res/favicon-16x16.png" sizes="16x16">
<link rel="icon" type="image/x-icon" href="/res/favicon.ico">
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="res/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="res/apple-touch-icon-144x144.png">
<link rel="icon" type="image/png" href="res/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="res/favicon-16x16.png" sizes="16x16">
<link rel="icon" type="image/x-icon" href="res/favicon.ico">
<link href="/css/source-sans-pro.min.css" rel="stylesheet">
<link href="/css/fontawesome.min.css" rel="stylesheet">
<link href="/css/stylesheet.css" rel="stylesheet">
<link href="/css/custom-dialog.css" rel="stylesheet">
<link href="/css/bookmark-item.css" rel="stylesheet">
<link href="css/source-sans-pro.min.css" rel="stylesheet">
<link href="css/fontawesome.min.css" rel="stylesheet">
<link href="css/stylesheet.css" rel="stylesheet">
<link href="css/custom-dialog.css" rel="stylesheet">
<link href="css/bookmark-item.css" rel="stylesheet">
<script src="/js/vue.min.js"></script>
<script src="/js/url.min.js"></script>
<script src="js/vue.min.js"></script>
<script src="js/url.min.js"></script>
</head>
<body class="night">
@ -84,22 +85,21 @@
secondText: "No",
mainClick: () => {
this.dialog.loading = true;
fetch("/api/logout", { method: "post" })
.then(response => {
if (!response.ok) throw response;
return response;
fetch(new URL("api/logout", document.baseURI), {
method: "post"
}).then(response => {
if (!response.ok) throw response;
return response;
}).then(() => {
localStorage.removeItem("shiori-account");
document.cookie = "session-id=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;";
location.href = new URL("login", document.baseURI);
}).catch(err => {
this.dialog.loading = false;
this.getErrorMessage(err).then(msg => {
this.showErrorDialog(msg);
})
.then(() => {
localStorage.removeItem("shiori-account");
document.cookie = "session-id=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;";
location.href = "/login";
})
.catch(err => {
this.dialog.loading = false;
this.getErrorMessage(err).then(msg => {
this.showErrorDialog(msg);
})
});
});
}
});
},

View File

@ -64,9 +64,13 @@ export default {
},
computed: {
mainURL() {
if (this.hasContent) return `/bookmark/${this.id}/content`;
else if (this.hasArchive) return `/bookmark/${this.id}/archive`;
else return this.url;
if (this.hasContent) {
return new URL(`bookmark/${this.id}/content`, document.baseURI);
} else if (this.hasArchive) {
return new URL(`bookmark/${this.id}/archive`, document.baseURI);
} else {
return this.url;
}
},
hostnameURL() {
var url = new URL(this.url);

View File

@ -81,7 +81,7 @@ export default {
}
},
isSessionError(err) {
switch (err.replace(/\(\d+\)/g, "").trim().toLowerCase()) {
switch (err.toString().replace(/\(\d+\)/g, "").trim().toLowerCase()) {
case "session is not exist":
case "session has been expired":
return true
@ -101,7 +101,7 @@ export default {
mainClick: () => {
this.dialog.visible = false;
if (sessionError) {
var loginUrl = new Url("/login");
var loginUrl = new Url("login", document.baseURI);
loginUrl.query.dst = window.location.href;
document.cookie = "session-id=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;";

View File

@ -197,7 +197,7 @@ export default {
keyword = keyword.trim().replace(/\s+/g, " ");
// Prepare URL for API
var url = new URL("/api/bookmarks", document.URL);
var url = new URL("api/bookmarks", document.baseURI);
url.search = new URLSearchParams({
keyword: keyword,
tags: tags.join(","),
@ -228,7 +228,7 @@ export default {
page: this.page
};
var url = new Url("/");
var url = new Url(document.baseURI);
url.hash = "home";
url.clearQuery();
if (this.page > 1) url.query.page = this.page;
@ -239,7 +239,7 @@ export default {
// Fetch tags if requested
if (fetchTags) {
return fetch("/api/tags");
return fetch(new URL("api/tags", document.baseURI));
} else {
this.loading = false;
throw skipFetchTags;
@ -408,7 +408,7 @@ export default {
};
this.dialog.loading = true;
fetch("/api/bookmarks", {
fetch(new URL("api/bookmarks", document.baseURI), {
method: "post",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" }
@ -497,7 +497,7 @@ export default {
// Send data
this.dialog.loading = true;
fetch("/api/bookmarks", {
fetch(new URL("api/bookmarks", document.baseURI), {
method: "put",
body: JSON.stringify(book),
headers: { "Content-Type": "application/json" }
@ -552,7 +552,7 @@ export default {
secondText: "No",
mainClick: () => {
this.dialog.loading = true;
fetch("/api/bookmarks", {
fetch(new URL("api/bookmarks", document.baseURI), {
method: "delete",
body: JSON.stringify(ids),
headers: { "Content-Type": "application/json" },
@ -622,7 +622,7 @@ export default {
};
this.dialog.loading = true;
fetch("/api/cache", {
fetch(new URL("api/cache", document.baseURI), {
method: "put",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
@ -700,7 +700,7 @@ export default {
}
this.dialog.loading = true;
fetch("/api/bookmarks/tags", {
fetch(new URL("api/bookmarks/tags", document.baseURI), {
method: "put",
body: JSON.stringify(request),
headers: { "Content-Type": "application/json" },
@ -766,7 +766,7 @@ export default {
};
this.dialog.loading = true;
fetch("/api/tag", {
fetch(new URL("api/tag", document.baseURI), {
method: "PUT",
body: JSON.stringify(newData),
headers: { "Content-Type": "application/json" },

View File

@ -98,7 +98,7 @@ export default {
if (this.loading) return;
this.loading = true;
fetch("/api/accounts")
fetch(new URL("api/accounts", document.baseURI))
.then(response => {
if (!response.ok) throw response;
return response.json();
@ -163,7 +163,7 @@ export default {
}
this.dialog.loading = true;
fetch("/api/accounts", {
fetch(new URL("api/accounts", document.baseURI), {
method: "post",
body: JSON.stringify(request),
headers: {
@ -246,7 +246,7 @@ export default {
}
this.dialog.loading = true;
fetch("/api/accounts", {
fetch(new URL("api/accounts", document.baseURI), {
method: "put",
body: JSON.stringify(request),
headers: {

View File

@ -2,23 +2,24 @@
<html lang="en">
<head>
<base href="$$.$$">
<title>Login - Shiori</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="/res/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/res/apple-touch-icon-144x144.png">
<link rel="icon" type="image/png" href="/res/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="/res/favicon-16x16.png" sizes="16x16">
<link rel="icon" type="image/x-icon" href="/res/favicon.ico">
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="res/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="res/apple-touch-icon-144x144.png">
<link rel="icon" type="image/png" href="res/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="res/favicon-16x16.png" sizes="16x16">
<link rel="icon" type="image/x-icon" href="res/favicon.ico">
<link href="/css/source-sans-pro.min.css" rel="stylesheet">
<link href="/css/fontawesome.min.css" rel="stylesheet">
<link href="/css/stylesheet.css" rel="stylesheet">
<link href="css/source-sans-pro.min.css" rel="stylesheet">
<link href="css/fontawesome.min.css" rel="stylesheet">
<link href="css/stylesheet.css" rel="stylesheet">
<script src="/js/vue.min.js"></script>
<script src="/js/url.min.js"></script>
<script src="js/vue.min.js"></script>
<script src="js/url.min.js"></script>
</head>
<body>
@ -82,7 +83,7 @@
// Send request
this.loading = true;
fetch("/api/login", {
fetch(new URL("api/login", document.baseURI), {
method: "post",
body: JSON.stringify({
username: this.username,
@ -106,7 +107,7 @@
dstPage = "";
}
var newUrl = new Url(dstUrl || "/");
var newUrl = new Url(dstUrl || document.baseURI);
newUrl.hash = dstPage;
location.href = newUrl;
}).catch(err => {

File diff suppressed because one or more lines are too long

View File

@ -42,7 +42,7 @@ func (h *handler) apiLogin(w http.ResponseWriter, r *http.Request, ps httprouter
// Save session ID to cache
strSessionID := sessionID.String()
h.SessionCache.Set(strSessionID, account.Owner, expTime)
h.SessionCache.Set(strSessionID, account, expTime)
// Save user's session IDs to cache as well
// useful for mass logout
@ -183,7 +183,7 @@ func (h *handler) apiGetBookmarks(w http.ResponseWriter, r *http.Request, ps htt
archivePath := fp.Join(h.DataDir, "archive", strID)
if fileExists(imgPath) {
bookmarks[i].ImageURL = path.Join("/", "bookmark", strID, "thumb")
bookmarks[i].ImageURL = path.Join(h.RootPath, "bookmark", strID, "thumb")
}
if fileExists(archivePath) {

View File

@ -4,10 +4,8 @@ import (
"bytes"
"compress/gzip"
"fmt"
"html/template"
"io"
"net/http"
nurl "net/url"
"os"
"path"
fp "path/filepath"
@ -15,32 +13,36 @@ import (
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/go-shiori/shiori/internal/model"
"github.com/go-shiori/shiori/pkg/warc"
"github.com/julienschmidt/httprouter"
)
// serveFile is handler for general file request
func (h *handler) serveFile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
err := serveFile(w, r.URL.Path, true)
rootPath := strings.Trim(h.RootPath, "/")
urlPath := strings.Trim(r.URL.Path, "/")
filePath := strings.TrimPrefix(urlPath, rootPath)
err := serveFile(w, filePath, true)
checkError(err)
}
// serveJsFile is handler for GET /js/*filepath
func (h *handler) serveJsFile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
filePath := r.URL.Path
fileName := path.Base(filePath)
fileDir := path.Dir(filePath)
jsFilePath := ps.ByName("filepath")
jsFilePath = path.Join("js", jsFilePath)
jsDir, jsName := path.Split(jsFilePath)
if developmentMode && fp.Ext(fileName) == ".js" && strings.HasSuffix(fileName, ".min.js") {
fileName = strings.TrimSuffix(fileName, ".min.js") + ".js"
filePath = path.Join(fileDir, fileName)
if assetExists(filePath) {
redirectPage(w, r, filePath)
return
if developmentMode && fp.Ext(jsName) == ".js" && strings.HasSuffix(jsName, ".min.js") {
jsName = strings.TrimSuffix(jsName, ".min.js") + ".js"
tmpPath := path.Join(jsDir, jsName)
if assetExists(tmpPath) {
jsFilePath = tmpPath
}
}
err := serveFile(w, r.URL.Path, true)
err := serveFile(w, jsFilePath, true)
checkError(err)
}
@ -49,12 +51,13 @@ func (h *handler) serveIndexPage(w http.ResponseWriter, r *http.Request, ps http
// Make sure session still valid
err := h.validateSession(r)
if err != nil {
redirectURL := createRedirectURL("/login", r.URL.String())
newPath := path.Join(h.RootPath, "/login")
redirectURL := createRedirectURL(newPath, r.URL.String())
redirectPage(w, r, redirectURL)
return
}
err = serveFile(w, "index.html", false)
err = h.templates["index"].Execute(w, h.RootPath)
checkError(err)
}
@ -63,11 +66,12 @@ func (h *handler) serveLoginPage(w http.ResponseWriter, r *http.Request, ps http
// Make sure session is not valid
err := h.validateSession(r)
if err == nil {
redirectPage(w, r, "/")
redirectURL := path.Join(h.RootPath, "/")
redirectPage(w, r, redirectURL)
return
}
err = serveFile(w, "login.html", false)
err = h.templates["login"].Execute(w, h.RootPath)
checkError(err)
}
@ -88,7 +92,8 @@ func (h *handler) serveBookmarkContent(w http.ResponseWriter, r *http.Request, p
if bookmark.Public != 1 {
err = h.validateSession(r)
if err != nil {
redirectURL := createRedirectURL("/login", r.URL.String())
newPath := path.Join(h.RootPath, "/login")
redirectURL := createRedirectURL(newPath, r.URL.String())
redirectPage(w, r, redirectURL)
return
}
@ -116,7 +121,7 @@ func (h *handler) serveBookmarkContent(w http.ResponseWriter, r *http.Request, p
// Find all image and convert its source to use the archive URL.
createArchivalURL := func(archivalName string) string {
archivalURL := *r.URL
archivalURL.Path = path.Join("/", "bookmark", strID, "archive", archivalName)
archivalURL.Path = path.Join(h.RootPath, "bookmark", strID, "archive", archivalName)
return archivalURL.String()
}
@ -162,18 +167,13 @@ func (h *handler) serveBookmarkContent(w http.ResponseWriter, r *http.Request, p
checkError(err)
}
// Create template
funcMap := template.FuncMap{
"html": func(s string) template.HTML {
return template.HTML(s)
},
}
tplCache, err := createTemplate("content.html", funcMap)
checkError(err)
// Execute template
err = tplCache.Execute(w, &bookmark)
tplData := struct {
RootPath string
Book model.Bookmark
}{h.RootPath, bookmark}
err = h.templates["content"].Execute(w, &tplData)
checkError(err)
}
@ -230,13 +230,9 @@ func (h *handler) serveBookmarkArchive(w http.ResponseWriter, r *http.Request, p
if bookmark.Public != 1 {
err = h.validateSession(r)
if err != nil {
urlQueries := nurl.Values{}
urlQueries.Set("dst", r.URL.Path)
redirectURL, _ := nurl.Parse("/login")
redirectURL.RawQuery = urlQueries.Encode()
redirectPage(w, r, redirectURL.String())
newPath := path.Join(h.RootPath, "/login")
redirectURL := createRedirectURL(newPath, r.URL.String())
redirectPage(w, r, redirectURL)
return
}
}
@ -274,23 +270,15 @@ func (h *handler) serveBookmarkArchive(w http.ResponseWriter, r *http.Request, p
checkError(err)
// Add Shiori overlay
tpl, err := template.New("archive").Parse(
`<div id="shiori-archive-header">
<p id="shiori-logo"><span></span>shiori</p>
<div class="spacer"></div>
<a href="{{.URL}}" target="_blank">View Original</a>
{{if .HasContent}}
<a href="/bookmark/{{.ID}}/content">View Readable</a>
{{end}}
</div>`)
checkError(err)
tplOutput := bytes.NewBuffer(nil)
err = tpl.Execute(tplOutput, &bookmark)
err = h.templates["archive"].Execute(tplOutput, &bookmark)
checkError(err)
doc.Find("head").AppendHtml(`<link href="/css/source-sans-pro.min.css" rel="stylesheet">`)
doc.Find("head").AppendHtml(`<link href="/css/archive.css" rel="stylesheet">`)
archiveCSSPath := path.Join(h.RootPath, "/css/archive.css")
sourceSansProCSSPath := path.Join(h.RootPath, "/css/source-sans-pro.min.css")
doc.Find("head").AppendHtml(`<link href="` + archiveCSSPath + `" rel="stylesheet">`)
doc.Find("head").AppendHtml(`<link href="` + sourceSansProCSSPath + `" rel="stylesheet">`)
doc.Find("body").PrependHtml(tplOutput.String())
// Revert back to HTML

View File

@ -2,9 +2,12 @@ package webserver
import (
"fmt"
"html/template"
"net/http"
"github.com/go-shiori/shiori/internal/database"
"github.com/go-shiori/shiori/internal/model"
"github.com/go-shiori/shiori/pkg/warc"
cch "github.com/patrickmn/go-cache"
)
@ -14,16 +17,18 @@ var developmentMode = false
type handler struct {
DB database.DB
DataDir string
RootPath string
UserCache *cch.Cache
SessionCache *cch.Cache
ArchiveCache *cch.Cache
templates map[string]*template.Template
}
// prepareLoginCache prepares login cache for future use
func (h *handler) prepareLoginCache() {
func (h *handler) prepareSessionCache() {
h.SessionCache.OnEvicted(func(key string, val interface{}) {
username := val.(string)
arr, found := h.UserCache.Get(username)
account := val.(model.Account)
arr, found := h.UserCache.Get(account.Username)
if !found {
return
}
@ -36,10 +41,54 @@ func (h *handler) prepareLoginCache() {
}
}
h.UserCache.Set(username, sessionIDs, -1)
h.UserCache.Set(account.Username, sessionIDs, -1)
})
}
func (h *handler) prepareArchiveCache() {
h.ArchiveCache.OnEvicted(func(key string, data interface{}) {
archive := data.(*warc.Archive)
archive.Close()
})
}
func (h *handler) prepareTemplates() error {
// Prepare variables
var err error
h.templates = make(map[string]*template.Template)
// Prepare func map
funcMap := template.FuncMap{
"html": func(s string) template.HTML {
return template.HTML(s)
},
}
// Create template for login, index and content
for _, name := range []string{"login", "index", "content"} {
h.templates[name], err = createTemplate(name+".html", funcMap)
if err != nil {
return err
}
}
// Create template for archive overlay
h.templates["archive"], err = template.New("archive").Delims("$$", "$$").Parse(
`<div id="shiori-archive-header">
<p id="shiori-logo"><span></span>shiori</p>
<div class="spacer"></div>
<a href="$$.URL$$" target="_blank">View Original</a>
$$if .HasContent$$
<a href="/bookmark/$$.ID$$/content">View Readable</a>
$$end$$
</div>`)
if err != nil {
return err
}
return nil
}
func (h *handler) getSessionID(r *http.Request) string {
// Get session-id from header and cookie
headerSessionID := r.Header.Get("X-Session-Id")
@ -76,7 +125,7 @@ func (h *handler) validateSession(r *http.Request) error {
// If this is not get request, make sure it's owner
if r.Method != "" && r.Method != "GET" {
if isOwner := val.(bool); !isOwner {
if account := val.(model.Account); !account.Owner {
return fmt.Errorf("account level is not sufficient")
}
}

View File

@ -3,62 +3,80 @@ package webserver
import (
"fmt"
"net/http"
"path"
"time"
"github.com/go-shiori/shiori/internal/database"
"github.com/go-shiori/shiori/pkg/warc"
"github.com/julienschmidt/httprouter"
cch "github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
// Config is parameter that used for starting web server
type Config struct {
DB database.DB
DataDir string
ServerAddress string
ServerPort int
RootPath string
}
// ServeApp serves wb interface in specified port
func ServeApp(DB database.DB, dataDir string, address string, port int) error {
func ServeApp(cfg Config) error {
// Create handler
hdl := handler{
DB: DB,
DataDir: dataDir,
DB: cfg.DB,
DataDir: cfg.DataDir,
UserCache: cch.New(time.Hour, 10*time.Minute),
SessionCache: cch.New(time.Hour, 10*time.Minute),
ArchiveCache: cch.New(time.Minute, 5*time.Minute),
RootPath: cfg.RootPath,
}
hdl.ArchiveCache.OnEvicted(func(key string, data interface{}) {
archive := data.(*warc.Archive)
archive.Close()
})
hdl.prepareSessionCache()
hdl.prepareArchiveCache()
err := hdl.prepareTemplates()
if err != nil {
return fmt.Errorf("failed to prepare templates: %v", err)
}
// Create router
router := httprouter.New()
router.GET("/js/*filepath", hdl.serveJsFile)
router.GET("/res/*filepath", hdl.serveFile)
router.GET("/css/*filepath", hdl.serveFile)
router.GET("/fonts/*filepath", hdl.serveFile)
// jp here means "join path", as in "join route with root path"
jp := func(route string) string {
return path.Join(cfg.RootPath, route)
}
router.GET("/", hdl.serveIndexPage)
router.GET("/login", hdl.serveLoginPage)
router.GET("/bookmark/:id/thumb", hdl.serveThumbnailImage)
router.GET("/bookmark/:id/content", hdl.serveBookmarkContent)
router.GET("/bookmark/:id/archive/*filepath", hdl.serveBookmarkArchive)
router.GET(jp("/js/*filepath"), hdl.serveJsFile)
router.GET(jp("/res/*filepath"), hdl.serveFile)
router.GET(jp("/css/*filepath"), hdl.serveFile)
router.GET(jp("/fonts/*filepath"), hdl.serveFile)
router.POST("/api/login", hdl.apiLogin)
router.POST("/api/logout", hdl.apiLogout)
router.GET("/api/bookmarks", hdl.apiGetBookmarks)
router.GET("/api/tags", hdl.apiGetTags)
router.PUT("/api/tag", hdl.apiRenameTag)
router.POST("/api/bookmarks", hdl.apiInsertBookmark)
router.DELETE("/api/bookmarks", hdl.apiDeleteBookmark)
router.PUT("/api/bookmarks", hdl.apiUpdateBookmark)
router.PUT("/api/cache", hdl.apiUpdateCache)
router.PUT("/api/bookmarks/tags", hdl.apiUpdateBookmarkTags)
router.POST("/api/bookmarks/ext", hdl.apiInsertViaExtension)
router.DELETE("/api/bookmarks/ext", hdl.apiDeleteViaExtension)
router.GET(jp("/"), hdl.serveIndexPage)
router.GET(jp("/login"), hdl.serveLoginPage)
router.GET(jp("/bookmark/:id/thumb"), hdl.serveThumbnailImage)
router.GET(jp("/bookmark/:id/content"), hdl.serveBookmarkContent)
router.GET(jp("/bookmark/:id/archive/*filepath"), hdl.serveBookmarkArchive)
router.GET("/api/accounts", hdl.apiGetAccounts)
router.PUT("/api/accounts", hdl.apiUpdateAccount)
router.POST("/api/accounts", hdl.apiInsertAccount)
router.DELETE("/api/accounts", hdl.apiDeleteAccount)
router.POST(jp("/api/login"), hdl.apiLogin)
router.POST(jp("/api/logout"), hdl.apiLogout)
router.GET(jp("/api/bookmarks"), hdl.apiGetBookmarks)
router.GET(jp("/api/tags"), hdl.apiGetTags)
router.PUT(jp("/api/tag"), hdl.apiRenameTag)
router.POST(jp("/api/bookmarks"), hdl.apiInsertBookmark)
router.DELETE(jp("/api/bookmarks"), hdl.apiDeleteBookmark)
router.PUT(jp("/api/bookmarks"), hdl.apiUpdateBookmark)
router.PUT(jp("/api/cache"), hdl.apiUpdateCache)
router.PUT(jp("/api/bookmarks/tags"), hdl.apiUpdateBookmarkTags)
router.POST(jp("/api/bookmarks/ext"), hdl.apiInsertViaExtension)
router.DELETE(jp("/api/bookmarks/ext"), hdl.apiDeleteViaExtension)
router.GET(jp("/api/accounts"), hdl.apiGetAccounts)
router.PUT(jp("/api/accounts"), hdl.apiUpdateAccount)
router.POST(jp("/api/accounts"), hdl.apiInsertAccount)
router.DELETE(jp("/api/accounts"), hdl.apiDeleteAccount)
// Route for panic
router.PanicHandler = func(w http.ResponseWriter, r *http.Request, arg interface{}) {
@ -66,7 +84,7 @@ func ServeApp(DB database.DB, dataDir string, address string, port int) error {
}
// Create server
url := fmt.Sprintf("%s:%d", address, port)
url := fmt.Sprintf("%s:%d", cfg.ServerAddress, cfg.ServerPort)
svr := &http.Server{
Addr: url,
Handler: router,