feat: remove viper dependency, add templating of upload url

This commit is contained in:
hgc 2024-02-11 13:43:02 +00:00
orang tua b5ffd0b493
melakukan 68da26718b
3 mengubah file dengan 82 tambahan dan 62 penghapusan

38
expiry.go Normal file
Melihat File

@ -0,0 +1,38 @@
package main
import (
"os"
"strconv"
"time"
"github.com/boltdb/bolt"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
func ExpiryDoer() {
for {
removed := 0
db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("expiry"))
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
expiryTime, err := strconv.ParseInt(string(v), 10, 64)
if err != nil {
log.Error().Err(err).Bytes("k", k).Bytes("v", v).Msg("Expiry time could not be parsed")
continue
}
if time.Now().After(time.Unix(expiryTime, 0)) {
os.Remove(viper.GetString("filedir") + "/" + string(k))
removed += 1
c.Delete()
}
}
return nil
})
if removed >= 1 {
log.Info().Int("amt", removed).Msg("Purged based on expiry")
}
time.Sleep(5 * time.Second)
}
}

Melihat File

@ -71,6 +71,6 @@ body{background-color:#1a1c1d;color:#fff;font-family:monospace;font-size:14px;pa
</div>
<script>const hole=document.getElementById("hole");const filelist=document.getElementById("filelist");const clearButton=document.getElementById("clear");const uploadButton=document.getElementById("upload");const urlLength=document.getElementById("url_len");const expiryTime=document.getElementById("expiry");let files=[];hole.addEventListener("dragover",(e)=>{e.preventDefault();hole.style.borderColor="#ccc";});hole.addEventListener("dragleave",()=>{hole.style.borderColor="#444";});hole.addEventListener("drop",(e)=>{e.preventDefault();hole.style.borderColor="#444";const newFiles=Array.from(e.dataTransfer.files);addFiles(newFiles);updateButtons();});hole.addEventListener("click",()=>{const input=document.createElement("input");input.type="file";input.multiple="true";input.accept="*";input.style.display="none";input.addEventListener("change",(e)=>{const newFiles=Array.from(e.target.files);addFiles(newFiles);updateButtons();});document.body.appendChild(input);input.click();document.body.removeChild(input);});const addFiles=(newFiles)=>{newFiles.forEach((file)=>{const listItem=document.createElement("li");listItem.classList.add("file");const fileName=document.createElement("span");fileName.textContent=file.name;listItem.appendChild(fileName);const fileSize=document.createElement("span");let sizeInBytes=file.size;if(sizeInBytes<1024){fileSize.textContent=sizeInBytes+" bytes";}else if(sizeInBytes<1024*1024){fileSize.textContent=(sizeInBytes/1024).toFixed(2)+" KB";}else if(sizeInBytes<1024*1024*1024){fileSize.textContent=(sizeInBytes/(1024*1024)).toFixed(2)+" MB";}else{fileSize.textContent=(sizeInBytes/(1024*1024*1024)).toFixed(2)+" GB";}
listItem.appendChild(fileSize);const removeButton=document.createElement("button");removeButton.textContent="X";removeButton.addEventListener("click",()=>{files.splice(files.indexOf(file),1);filelist.removeChild(listItem);updateButtons();});listItem.appendChild(removeButton);filelist.appendChild(listItem);files.push(file);});};const updateButtons=()=>{if(files.length>0){uploadButton.disabled=false;clearButton.disabled=false;}else{uploadButton.disabled=true;clearButton.disabled=true;}};clearButton.addEventListener("click",()=>{files=[];filelist.innerHTML="";updateButtons();});const dots=document.createElement("span");dots.textContent=".";let dotCount=0;uploadButton.addEventListener("click",()=>{const urlLengthValue=urlLength.value;const expiryTimeValue=expiryTime.value;let currentIndex=0;const updateItemStatus=(index,message)=>{const listItem=filelist.children[index];const statusElement=listItem.children[2];statusElement.textContent=message;};const uploadFile=()=>{if(currentIndex>=files.length){files=[];return;}
const file=files[currentIndex];const formData=new FormData();formData.append("file",file);formData.append("url_len",urlLengthValue);formData.append("expiry",expiryTimeValue);updateItemStatus(currentIndex,"uploading...");fetch("https://filehole.org/",{method:"POST",body:formData,}).then((response)=>response.text()).then((data)=>{data=data.trim();const url=document.createElement("a");url.href=data;url.target="_blank";url.textContent=data;const listItem=filelist.children[currentIndex];const removeButton=listItem.children[2];listItem.replaceChild(url,removeButton);const copyButton=document.createElement("button");copyButton.textContent="copy to clipboard";copyButton.addEventListener("click",(event)=>{event.stopPropagation();navigator.clipboard.writeText(data);});listItem.appendChild(copyButton);formData.delete("file");currentIndex++;uploadFile();}).catch((error)=>{console.error(error);updateItemStatus(currentIndex,"upload failed");});};uploadFile();});</script>
const file=files[currentIndex];const formData=new FormData();formData.append("file",file);formData.append("url_len",urlLengthValue);formData.append("expiry",expiryTimeValue);updateItemStatus(currentIndex,"uploading...");fetch("{{ .PublicUrl }}",{method:"POST",body:formData,}).then((response)=>response.text()).then((data)=>{data=data.trim();const url=document.createElement("a");url.href=data;url.target="_blank";url.textContent=data;const listItem=filelist.children[currentIndex];const removeButton=listItem.children[2];listItem.replaceChild(url,removeButton);const copyButton=document.createElement("button");copyButton.textContent="copy to clipboard";copyButton.addEventListener("click",(event)=>{event.stopPropagation();navigator.clipboard.writeText(data);});listItem.appendChild(copyButton);formData.delete("file");currentIndex++;uploadFile();}).catch((error)=>{console.error(error);updateItemStatus(currentIndex,"upload failed");});};uploadFile();});</script>
</body>
</html>

104
main.go
Melihat File

@ -3,7 +3,9 @@ package main
import (
"crypto/rand"
_ "embed"
"flag"
"html"
"html/template"
"io"
"net/http"
"os"
@ -15,8 +17,6 @@ import (
"github.com/gorilla/mux"
"github.com/landlock-lsm/go-landlock/landlock"
"github.com/spf13/viper"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
@ -36,20 +36,20 @@ func shortID(length int64) string {
var db *bolt.DB
func GalleryHandler(w http.ResponseWriter, r *http.Request) {
func (fh FileholeServer) GalleryHandler(w http.ResponseWriter, r *http.Request) {
v := mux.Vars(r)
w.Write([]byte(`<!DOCTYPE html><html><head><style>body { background-color: black; color: white; }</style></head><body>`))
for _, i := range strings.Split(v["files"], ",") {
link := viper.GetString("vhost") + `/u/` + i
link := fh.PublicUrl + `/u/` + i
w.Write([]byte(`<p>` + html.EscapeString(i) + `</p><a href="` + html.EscapeString(link) + `">` + `<img width=500em src="` + html.EscapeString(link) + `"></img></a>`))
}
w.Write([]byte(`</body></html>`))
}
func UploadHandler(w http.ResponseWriter, r *http.Request) {
func (fh FileholeServer) UploadHandler(w http.ResponseWriter, r *http.Request) {
// url_len sanitize
inpUrlLen := r.FormValue("url_len")
sanUrlLen, err := strconv.ParseInt(inpUrlLen, 10, 64)
@ -101,7 +101,7 @@ func UploadHandler(w http.ResponseWriter, r *http.Request) {
log.Info().Str("mtype", mtype.String()).Str("ext", mtype.Extension()).Int64("expiry", sanExpiry).Int64("url_len", sanUrlLen).Msg("Writing new file")
f, err := os.OpenFile(viper.GetString("filedir")+"/"+name, os.O_WRONLY|os.O_CREATE, 0644)
f, err := os.OpenFile(fh.StorageDir+"/"+name, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
log.Error().Err(err).Msg("Error opening a file for write")
w.Write([]byte("internal error\n"))
@ -111,62 +111,39 @@ func UploadHandler(w http.ResponseWriter, r *http.Request) {
io.Copy(f, file)
w.Write([]byte(viper.GetString("vhost") + "/u/" + name + "\n"))
}
func ExpiryDoer() {
for {
removed := 0
db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("expiry"))
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
expiryTime, err := strconv.ParseInt(string(v), 10, 64)
if err != nil {
log.Error().Err(err).Bytes("k", k).Bytes("v", v).Msg("Expiry time could not be parsed")
continue
}
if time.Now().After(time.Unix(expiryTime, 0)) {
os.Remove(viper.GetString("filedir") + "/" + string(k))
removed += 1
c.Delete()
}
}
return nil
})
if removed >= 1 {
log.Info().Int("amt", removed).Msg("Purged based on expiry")
}
time.Sleep(5 * time.Second)
}
w.Write([]byte(fh.PublicUrl + "/u/" + name + "\n"))
}
//go:embed index.html
var indexPage []byte
type FileholeServer struct {
Bind string // Address to bind ex. 127.0.0.1:8000
MetadataFile string // File metadata storage KV store filename ex. filehole.db
StorageDir string // Data storage folder ex. /data
PublicUrl string // The internet facing path of the site i.e. https://filehole.org
}
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
func main() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
viper.SetDefault("bind", "127.0.0.1:8000")
viper.SetDefault("database", "filehole.db")
viper.SetDefault("filedir", "./data")
viper.SetDefault("vhost", "http://127.0.0.1:8000")
fh := FileholeServer{}
flag.StringVar(&fh.Bind, "bind", getEnv("FH_BIND", "127.0.0.1:8000"), "Address to bind ENV: FH_BIND")
flag.StringVar(&fh.MetadataFile, "metadata-path", getEnv("FH_METADATA_FILE", "filehole.db"), "File metadata storage KV store filename ENV: FH_METADATA_FILE")
flag.StringVar(&fh.StorageDir, "storage-dir", getEnv("FH_STORAGE_DIR", "./data"), "Data storage folder ENV: FH_STORAGE_DIR")
flag.StringVar(&fh.PublicUrl, "public-url", getEnv("FH_PUBLIC_URL", "https://filehole.org"), "Internet facing URL of the base of the site ENV: FH_PUBLIC_URL")
viper.SetConfigName("config")
viper.SetConfigType("toml")
viper.AddConfigPath("/etc/filehole/")
viper.AddConfigPath(".")
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
log.Error().Err(err).Msg("Failed to load a config file")
} else {
log.Info().Msg("Created config file since none were located")
}
}
flag.Parse()
var err error
db, err = bolt.Open(viper.GetString("database"), 0600, nil)
db, err = bolt.Open(fh.MetadataFile, 0600, nil)
if err != nil {
log.Fatal().Err(err).Msg("dangerous database activity")
}
@ -179,14 +156,13 @@ func main() {
return nil
})
// New single binary setup will have this user only reading from data
os.Mkdir(viper.GetString("filedir"), 0600)
os.Mkdir(fh.StorageDir, 0600)
// We actually need to landlock after creating all the files we reference
// in the landlock or it will fail
err = landlock.V2.BestEffort().RestrictPaths(
landlock.RWDirs(viper.GetString("filedir")),
landlock.RWFiles(viper.GetString("database")),
landlock.RWDirs(fh.StorageDir),
landlock.RWFiles(fh.MetadataFile),
)
if err != nil {
log.Error().Err(err).Msg("Could not landlock")
@ -204,20 +180,26 @@ func main() {
r := mux.NewRouter()
// Serve multiple images in a gallery
r.HandleFunc("/g/{files}", GalleryHandler)
r.HandleFunc("/g/{files}", fh.GalleryHandler)
// Serve files from data dir statically
r.PathPrefix("/u/").Handler(http.StripPrefix("/u/", NoDirectoryList(http.FileServer(http.Dir(viper.GetString("filedir"))))))
r.PathPrefix("/u/").Handler(http.StripPrefix("/u/", NoDirectoryList(http.FileServer(http.Dir(fh.StorageDir)))))
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write(indexPage)
r.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
t, _ := template.New("index").Parse(string(indexPage))
t.Execute(w, map[string]interface{}{
"PublicUrl": fh.PublicUrl,
})
// w.Write(indexPage)
}).Methods("GET")
r.HandleFunc("/", UploadHandler).Methods("POST")
r.HandleFunc("/", fh.UploadHandler).Methods("POST")
http.Handle("/", r)
go ExpiryDoer()
http.ListenAndServe(viper.GetString("bind"), r)
http.ListenAndServe(fh.Bind, r)
db.Close()
}