Skip to content

Instantly share code, notes, and snippets.

@jjkavalam
Last active January 9, 2024 16:32
Show Gist options
  • Save jjkavalam/cf07e51ac6e5b309e468bfccb4aae541 to your computer and use it in GitHub Desktop.
Save jjkavalam/cf07e51ac6e5b309e468bfccb4aae541 to your computer and use it in GitHub Desktop.
TiddlyWiki Saver Server
// From a directory that contains your TiddlyWiki file (or download https://tiddlywiki.com/empty.html if you don't have one)
// `go run server.go`
// Navigate to the printed URL and open your wiki file
// Enjoy your changes getting saved ! (A backup of the current state is made to the `bak` directory before overwriting)
// You may configure the number of backups to keep below (See `numBackups`)
package main
import (
"fmt"
"io"
"io/fs"
"log"
"net/http"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
)
const backupBaseDir = "bak"
const numBackups = 5
func main() {
var root string
if len(os.Args) != 1 {
root = os.Args[1]
} else {
root = "."
}
fileServerHandler := http.FileServer(http.Dir(root))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
fileServerHandler.ServeHTTP(w, r)
case "PUT":
handlePut(w, r, root)
case "OPTIONS":
handleOptions(w, r)
default:
w.WriteHeader(200)
}
})
server := &http.Server{
Addr: "127.0.0.1:8000",
Handler: http.DefaultServeMux,
}
go func() {
if err := server.ListenAndServe(); err != nil {
fmt.Println(err)
}
}()
fmt.Println("TiddlyWiki Backup Server is running at http://127.0.0.1:8000/")
fmt.Println("Press Ctrl+C to shutdown...")
select {}
}
func handlePut(w http.ResponseWriter, r *http.Request, root string) {
backupDir := filepath.Join(root, backupBaseDir, r.URL.Path)
filePath := filepath.Join(root, r.URL.Path)
if strings.HasPrefix(r.URL.Path, "/"+backupBaseDir+"/") {
// the file being saved is itself inside the backup dir
w.WriteHeader(http.StatusBadRequest)
return
}
writeErr := func(err error) {
log.Printf("error: %s", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
if err := os.MkdirAll(backupDir, os.ModePerm); err != nil {
writeErr(err)
return
}
// List existing backup files
files, err := os.ReadDir(backupDir)
if err != nil {
writeErr(err)
return
}
var fInfo1, fInfo2 fs.FileInfo
var sErr error
// Sort files by modification time (oldest first)
sort.Slice(files, func(i, j int) bool {
if sErr != nil {
return false
}
fInfo1, sErr = files[i].Info()
if sErr != nil {
return false
}
fInfo2, sErr = files[j].Info()
if sErr != nil {
return false
}
return fInfo1.ModTime().Before(fInfo2.ModTime())
})
if sErr != nil {
writeErr(sErr)
return
}
var backupPath string
if len(files) < numBackups {
backupPath = filepath.Join(backupDir, strconv.Itoa(len(files)+1))
} else {
backupPath = filepath.Join(backupDir, files[0].Name())
}
fmt.Print(".")
src, err := os.Open(filePath)
if err != nil {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
defer src.Close()
dst, err := os.Create(backupPath)
if err != nil {
writeErr(err)
return
}
defer dst.Close()
if _, err := io.Copy(dst, src); err != nil {
writeErr(err)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
writeErr(err)
return
}
if err := os.WriteFile(filePath, body, os.ModePerm); err != nil {
writeErr(err)
return
}
w.WriteHeader(http.StatusOK)
}
func handleOptions(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Allow", "GET,HEAD,OPTIONS,PUT")
// TiddlyWiki looks for a 'dav' header, and ignores any value
w.Header().Set("dav", "anything")
}
@jjkavalam
Copy link
Author

@jjkavalam
Copy link
Author

# run tiddly server at the current path
alias tiddly='go run path/to/server.go `pwd`'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment