Add Calendar integration (read-only)
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
_ "time/tzdata" // calendar TZIDs must resolve even without system zoneinfo
|
||||
)
|
||||
|
||||
//go:embed assets
|
||||
@@ -38,10 +39,13 @@ func hashAsset(name string) string {
|
||||
}
|
||||
|
||||
// tmplFuncs is shared by every layout-based template: the edit template appends
|
||||
// editorBundleVersion to its CodeMirror <script> src to cache-bust the bundle.
|
||||
// editorBundleVersion to its CodeMirror <script> src to cache-bust the bundle;
|
||||
// the layout header renders the maintenance widget.
|
||||
var tmplFuncs = template.FuncMap{
|
||||
"editorBundleVersion": func() string { return editorBundleVersion },
|
||||
"fileIcon": fileIcon,
|
||||
"icon": readIcon,
|
||||
"maintenanceWidget": maintenanceWidget,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -88,8 +92,18 @@ func main() {
|
||||
user := flag.String("user", "", "basic auth username (empty = no auth)")
|
||||
pass := flag.String("pass", "", "basic auth password")
|
||||
reindexInterval := flag.Duration("reindex-interval", 30*time.Minute, "periodic search index rebuild interval (0 disables)")
|
||||
calendars := flag.String("calendars", "Calendars", "calendars folder, relative to -dir (empty disables)")
|
||||
tz := flag.String("tz", "", "time zone for calendars and the diary, e.g. Europe/Berlin (default: system)")
|
||||
flag.Parse()
|
||||
|
||||
if *tz != "" {
|
||||
loc, err := time.LoadLocation(*tz)
|
||||
if err != nil {
|
||||
log.Fatalf("-tz: %v", err)
|
||||
}
|
||||
time.Local = loc
|
||||
}
|
||||
|
||||
root, err := filepath.Abs(*wikiDir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -108,6 +122,15 @@ func main() {
|
||||
|
||||
initMarkdown()
|
||||
|
||||
if *calendars != "" {
|
||||
calendarsDir = filepath.Join(root, filepath.FromSlash(*calendars))
|
||||
rel, err := filepath.Rel(root, calendarsDir)
|
||||
if err != nil || rel == "." || strings.HasPrefix(rel, "..") {
|
||||
log.Fatalf("-calendars must be a folder inside -dir")
|
||||
}
|
||||
calendarsURL = "/" + filepath.ToSlash(rel) + "/"
|
||||
}
|
||||
|
||||
authKey, err := loadOrCreateAuthKey(root)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -158,6 +181,10 @@ func main() {
|
||||
}(*reindexInterval)
|
||||
}
|
||||
|
||||
if calendarsDir != "" {
|
||||
startCalendarSync()
|
||||
}
|
||||
|
||||
log.Printf("datascape listening on %s, wiki at %s", *addr, root)
|
||||
log.Fatal(http.ListenAndServe(*addr, nil))
|
||||
}
|
||||
@@ -188,6 +215,14 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Dotfiles are never served: .page-settings can hold the CalDAV
|
||||
// password and .auth-key signs sessions. Nothing links to them (listing,
|
||||
// tree and search hide them already).
|
||||
if hasDotSegment(r.URL.Path) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(r.URL.Path, thumbURLPrefix+"/") {
|
||||
h.handleThumb(w, r)
|
||||
return
|
||||
@@ -255,6 +290,17 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// hasDotSegment reports whether any segment of the cleaned URL path starts
|
||||
// with a dot.
|
||||
func hasDotSegment(p string) bool {
|
||||
for _, seg := range strings.Split(path.Clean("/"+p), "/") {
|
||||
if strings.HasPrefix(seg, ".") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// slashedURL returns u's path with a trailing slash appended and the query
|
||||
// string intact — the canonical page form that relative destinations resolve
|
||||
// against.
|
||||
@@ -414,6 +460,10 @@ func (h *handler) handlePost(w http.ResponseWriter, r *http.Request, urlPath, fs
|
||||
h.handleSettings(w, r, urlPath, fsPath)
|
||||
return
|
||||
}
|
||||
if query.Has("calsync") {
|
||||
h.handleCalendarSync(w, r, urlPath, fsPath)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
@@ -525,12 +575,28 @@ func (h *handler) handlePost(w http.ResponseWriter, r *http.Request, urlPath, fs
|
||||
// Returns nil if the file does not exist.
|
||||
// Format: one "key = value" pair per line; lines starting with # are comments.
|
||||
func readPageSettings(dir string) *pageSettings {
|
||||
kv := readSettingsFile(dir)
|
||||
if kv == nil {
|
||||
return nil
|
||||
}
|
||||
// Unknown or missing values fall back to the defaults.
|
||||
return &pageSettings{
|
||||
Type: kv["type"],
|
||||
View: validateView(kv["view"]),
|
||||
Sort: validateSort(kv["sort"]),
|
||||
Order: validateOrder(kv["order"]),
|
||||
}
|
||||
}
|
||||
|
||||
// readSettingsFile returns every key of dir's .page-settings (last one wins),
|
||||
// or nil if the file does not exist. Other features (e.g. the calendars
|
||||
// folder) keep their own keys in the same file.
|
||||
func readSettingsFile(dir string) map[string]string {
|
||||
data, err := os.ReadFile(filepath.Join(dir, ".page-settings"))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
// Defaults; overridden only by valid values present in the file.
|
||||
s := &pageSettings{View: viewList, Sort: sortName, Order: orderAsc}
|
||||
kv := map[string]string{}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
@@ -540,17 +606,7 @@ func readPageSettings(dir string) *pageSettings {
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
value := strings.TrimSpace(parts[1])
|
||||
switch strings.TrimSpace(parts[0]) {
|
||||
case "type":
|
||||
s.Type = value
|
||||
case "view":
|
||||
s.View = validateView(value)
|
||||
case "sort":
|
||||
s.Sort = validateSort(value)
|
||||
case "order":
|
||||
s.Order = validateOrder(value)
|
||||
}
|
||||
kv[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||
}
|
||||
return s
|
||||
return kv
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user