package main
import (
"fmt"
"html/template"
"log"
"net/http"
"os"
"path/filepath"
"strings"
)
var templateDir = getTemplateDir()
var funcMap = template.FuncMap{
"normalizeStr": normalizeStr,
}
var indexTemp = template.Must(template.New("index.html").Funcs(funcMap).ParseFiles(
filepath.Join(templateDir, "layout.html"),
filepath.Join(templateDir, "index.html"),
filepath.Join(templateDir, "topics.html"),
filepath.Join(templateDir, "list.html"),
))
var adminTemp = template.Must(template.New("admin.html").Funcs(funcMap).ParseFiles(
filepath.Join(templateDir, "admin.html"),
filepath.Join(templateDir, "layout.html"),
filepath.Join(templateDir, "topics.html"),
filepath.Join(templateDir, "list.html"),
))
var editTemp = template.Must(template.New("admin-edit.html").Funcs(funcMap).ParseFiles(
filepath.Join(templateDir, "admin-edit.html"),
filepath.Join(templateDir, "layout.html"),
filepath.Join(templateDir, "topics.html"),
filepath.Join(templateDir, "list.html"),
))
func normalizeStr(s string) string {
trim := strings.TrimPrefix(strings.TrimSuffix(s, "\n"), "\n")
return strings.Join(strings.Fields(trim), " ")
}
// getTemplateDir returns the absolute path of the templates directory,
// preferring system-installed assets over the project-local path
func getTemplateDir() string {
if _, err := os.Stat(filepath.Join(buildPrefix,
"/share/crane/templates")); err != nil {
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
log.Fatal(err)
}
return filepath.Join(dir, "templates")
} else {
return filepath.Join(buildPrefix, "/share/crane/templates")
}
}
// IndexHandler renders the index of papers stored in papers.Path
func (papers *Papers) IndexHandler(w http.ResponseWriter, r *http.Request) {
// catch-all for paths unhandled by direct http.HandleFunc calls
if r.URL.Path != "/" {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
res := Resp{Papers: papers}
err := indexTemp.Execute(w, &res)
if err != nil {
fmt.Println(err)
}
}
// AdminHandler renders the index of papers stored in papers.Path with
// additional forms to modify the collection (add, delete, rename...)
func (papers *Papers) AdminHandler(w http.ResponseWriter, r *http.Request) {
res := Resp{Papers: papers}
if user != "" && pass != "" {
username, password, ok := r.BasicAuth()
if ok && user == username && pass == password {
adminTemp.Execute(w, &res)
} else {
w.Header().Add("WWW-Authenticate",
`Basic realm="Please authenticate"`)
http.Error(w, http.StatusText(http.StatusUnauthorized),
http.StatusUnauthorized)
}
} else {
adminTemp.Execute(w, &res)
}
}
// EditHandler renders the index of papers stored in papers.Path, prefixing
// a checkbox to each unique paper and category for modification
func (papers *Papers) EditHandler(w http.ResponseWriter, r *http.Request) {
res := Resp{Papers: papers}
if user != "" && pass != "" {
username, password, ok := r.BasicAuth()
if !ok || user != username || pass != password {
w.Header().Add("WWW-Authenticate",
`Basic realm="Please authenticate"`)
http.Error(w, http.StatusText(http.StatusUnauthorized),
http.StatusUnauthorized)
return
}
}
if err := r.ParseForm(); err != nil {
res.Status = err.Error()
editTemp.Execute(w, &res)
return
}
if action := r.FormValue("action"); action == "delete" {
for _, paper := range r.Form["paper"] {
if res.Status != "" {
break
}
if err := papers.DeletePaper(paper); err != nil {
res.Status = err.Error()
}
}
for _, category := range r.Form["category"] {
if res.Status != "" {
break
}
if err := papers.DeleteCategory(category); err != nil {
res.Status = err.Error()
}
}
if res.Status == "" {
res.Status = "delete successful"
}
} else if strings.HasPrefix(action, "move") {
destCategory := strings.SplitN(action, "move-", 2)[1]
for _, paper := range r.Form["paper"] {
if res.Status != "" {
break
}
if err := papers.MovePaper(paper, destCategory); err != nil {
res.Status = err.Error()
}
}
if res.Status == "" {
res.Status = "move successful"
}
} else {
rc := r.FormValue("rename-category")
rt := r.FormValue("rename-to")
if rc != "" && rt != "" {
// ensure filesystem safety of category names
rc = strings.Trim(strings.Replace(rc, "..", "", -1), "/.")
rt = strings.Trim(strings.Replace(rt, "..", "", -1), "/.")
if err := papers.RenameCategory(rc, rt); err != nil {
res.Status = err.Error()
}
if res.Status == "" {
res.Status = "rename successful"
}
}
}
editTemp.Execute(w, &res)
}
// AddHandler provides support for new paper processing and category addition
func (papers *Papers) AddHandler(w http.ResponseWriter, r *http.Request) {
if user != "" && pass != "" {
username, password, ok := r.BasicAuth()
if !ok || user != username || pass != password {
w.Header().Add("WWW-Authenticate",
`Basic realm="Please authenticate"`)
http.Error(w, http.StatusText(http.StatusUnauthorized),
http.StatusUnauthorized)
return
}
}
p := r.FormValue("dl-paper")
c := r.FormValue("dl-category")
nc := r.FormValue("new-category")
// sanitize input; we use the category to build the path used to save
// papers
nc = strings.Trim(strings.Replace(nc, "..", "", -1), "/.")
res := Resp{}
// paper download, both required fields populated
if len(strings.TrimSpace(p)) > 0 && len(strings.TrimSpace(c)) > 0 {
if paper, err := papers.ProcessAddPaperInput(c, p); err != nil {
res.Status = err.Error()
} else {
if paper.Meta.Title != "" {
res.Status = fmt.Sprintf("%q downloaded successfully",
paper.Meta.Title)
} else {
res.Status = fmt.Sprintf("%q downloaded successfully",
paper.PaperName)
}
res.LastPaperDL = strings.TrimPrefix(paper.PaperPath,
papers.Path+"/")
}
res.LastUsedCategory = c
} else if len(strings.TrimSpace(nc)) > 0 {
// accounts for nested category addition; e.g. "foo/bar/baz" where
// "foo/bar" and/or "foo" do not already exist
n := nc
for n != "." {
_, exists := papers.List[n]
if exists == true {
res.Status = fmt.Sprintf("category %q already exists", n)
} else if err := os.MkdirAll(filepath.Join(papers.Path, n),
os.ModePerm); err != nil {
res.Status = fmt.Sprintf(err.Error())
} else {
papers.List[n] = make(map[string]*Paper)
}
if res.Status != "" {
break
}
res.LastUsedCategory = n
n = filepath.Dir(n)
}
if res.Status == "" {
res.Status = fmt.Sprintf("category %q added successfully", nc)
}
}
res.Papers = papers
adminTemp.Execute(w, &res)
}
// DownloadHandler serves saved papers up for download
func (papers *Papers) DownloadHandler(w http.ResponseWriter, r *http.Request) {
paper := strings.TrimPrefix(r.URL.Path, "/download/")
category := filepath.Dir(paper)
// return 404 if the provided paper category or paper key do not exist in
// the papers set
if _, exists := papers.List[category]; exists == false {
http.Error(w, http.StatusText(http.StatusNotFound),
http.StatusNotFound)
return
}
if _, exists := papers.List[category][paper]; exists == false {
http.Error(w, http.StatusText(http.StatusNotFound),
http.StatusNotFound)
return
}
// ensure the paper (PaperPath) actually exists on the filesystem
i, err := os.Stat(papers.List[category][paper].PaperPath)
if os.IsNotExist(err) {
http.Error(w, http.StatusText(http.StatusNotFound),
http.StatusNotFound)
} else if i.IsDir() {
http.Error(w, http.StatusText(http.StatusForbidden),
http.StatusForbidden)
} else {
http.ServeFile(w, r, papers.List[category][paper].PaperPath)
}
}