feat: remove viper dependency, add templating of upload url
This commit is contained in:
orang tua
b5ffd0b493
melakukan
68da26718b
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
104
main.go
|
@ -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()
|
||||
}
|
||||
|
|
Memuat…
Reference in New Issue