mirror of
https://github.com/go-shiori/shiori.git
synced 2025-03-13 00:21:35 +08:00
Initial support for subpath #39
This commit is contained in:
parent
3077c7fbb8
commit
99d27930ea
13 changed files with 298 additions and 210 deletions
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;";
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue