mirror of
https://github.com/Forceu/Gokapi.git
synced 2026-03-13 13:39:02 -05:00
Added hotlinking for image files
This commit is contained in:
2
Main.go
2
Main.go
@@ -31,7 +31,7 @@ func main() {
|
||||
configuration.Load()
|
||||
checkArguments()
|
||||
go storage.CleanUp(true)
|
||||
webserver.Start(staticFolderEmbedded, templateFolderEmbedded)
|
||||
webserver.Start(&staticFolderEmbedded, &templateFolderEmbedded)
|
||||
}
|
||||
|
||||
// Checks for command line arguments that have to be parsed before loading the configuration
|
||||
|
||||
@@ -34,7 +34,7 @@ var Environment environment.Environment
|
||||
var ServerSettings Configuration
|
||||
|
||||
// Version of the configuration structure. Used for upgrading
|
||||
const currentConfigVersion = 3
|
||||
const currentConfigVersion = 4
|
||||
|
||||
// Struct that contains the global configuration
|
||||
type Configuration struct {
|
||||
@@ -48,6 +48,7 @@ type Configuration struct {
|
||||
RedirectUrl string `json:"RedirectUrl"`
|
||||
Sessions map[string]sessionstructure.Session `json:"Sessions"`
|
||||
Files map[string]filestructure.File `json:"Files"`
|
||||
Hotlinks map[string]filestructure.Hotlink `json:"Hotlinks"`
|
||||
ConfigVersion int `json:"ConfigVersion"`
|
||||
SaltAdmin string `json:"SaltAdmin"`
|
||||
SaltFiles string `json:"SaltFiles"`
|
||||
@@ -86,6 +87,10 @@ func updateConfig() {
|
||||
ServerSettings.LengthId = 15
|
||||
ServerSettings.DataDir = Environment.DataDir
|
||||
}
|
||||
// < v1.1.3
|
||||
if ServerSettings.ConfigVersion < 4 {
|
||||
ServerSettings.Hotlinks = make(map[string]filestructure.Hotlink)
|
||||
}
|
||||
|
||||
if ServerSettings.ConfigVersion < currentConfigVersion {
|
||||
fmt.Println("Successfully upgraded database")
|
||||
@@ -126,6 +131,7 @@ func generateDefaultConfig() {
|
||||
RedirectUrl: redirect,
|
||||
Files: make(map[string]filestructure.File),
|
||||
Sessions: make(map[string]sessionstructure.Session),
|
||||
Hotlinks: make(map[string]filestructure.Hotlink),
|
||||
ConfigVersion: currentConfigVersion,
|
||||
SaltAdmin: saltAdmin,
|
||||
SaltFiles: saltFiles,
|
||||
|
||||
@@ -57,3 +57,12 @@ func Check(e error) {
|
||||
panic(e)
|
||||
}
|
||||
}
|
||||
|
||||
func IsInArray(haystack []string, needle string) bool {
|
||||
for _, item := range haystack {
|
||||
if needle == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// A rune array to be used for pseudo-random string generation
|
||||
@@ -44,7 +45,7 @@ func GenerateRandomString(length int) string {
|
||||
if err != nil {
|
||||
return generateUnsafeId(length)
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(b)
|
||||
return cleanRandomString(base64.URLEncoding.EncodeToString(b))
|
||||
}
|
||||
|
||||
// Converts bytes to a human readable format
|
||||
@@ -61,3 +62,13 @@ func ByteCountSI(b int64) string {
|
||||
return fmt.Sprintf("%.1f %cB",
|
||||
float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
|
||||
// Removes special characters from string
|
||||
func cleanRandomString(input string) string {
|
||||
reg, err := regexp.Compile("[^a-zA-Z0-9]+")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return reg.ReplaceAllString(input, "")
|
||||
|
||||
}
|
||||
|
||||
@@ -11,9 +11,13 @@ import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -41,6 +45,7 @@ func NewFile(fileContent *multipart.File, fileHeader *multipart.FileHeader, expi
|
||||
DownloadsRemaining: downloads,
|
||||
PasswordHash: configuration.HashPassword(password, true),
|
||||
}
|
||||
addHotlink(&file)
|
||||
configuration.ServerSettings.Files[id] = file
|
||||
filename := configuration.ServerSettings.DataDir + "/" + file.SHA256
|
||||
if !helper.FileExists(configuration.ServerSettings.DataDir + "/" + file.SHA256) {
|
||||
@@ -55,6 +60,68 @@ func NewFile(fileContent *multipart.File, fileHeader *multipart.FileHeader, expi
|
||||
return file, nil
|
||||
}
|
||||
|
||||
var imageFileExtensions = []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg"}
|
||||
|
||||
// If file is an image, create link for hotlinking
|
||||
func addHotlink(file *filestructure.File) {
|
||||
extension := strings.ToLower(filepath.Ext(file.Name))
|
||||
if !helper.IsInArray(imageFileExtensions, extension) {
|
||||
return
|
||||
}
|
||||
link := helper.GenerateRandomString(40) + extension
|
||||
file.HotlinkId = link
|
||||
configuration.ServerSettings.Hotlinks[link] = filestructure.Hotlink{
|
||||
Id: link,
|
||||
FileId: file.Id,
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the file by id. Returns (empty File, false) if invalid / expired file
|
||||
// or (file, true) if valid file
|
||||
func GetFile(id string) (filestructure.File, bool) {
|
||||
var emptyResult = filestructure.File{}
|
||||
if id == "" {
|
||||
return emptyResult, false
|
||||
}
|
||||
file := configuration.ServerSettings.Files[id]
|
||||
if file.ExpireAt < time.Now().Unix() || file.DownloadsRemaining < 1 {
|
||||
return emptyResult, false
|
||||
}
|
||||
if !helper.FileExists(configuration.ServerSettings.DataDir + "/" + file.SHA256) {
|
||||
return emptyResult, false
|
||||
}
|
||||
return file, true
|
||||
}
|
||||
|
||||
// Gets the file by hotlink id. Returns (empty File, false) if invalid / expired file
|
||||
// or (file, true) if valid file
|
||||
func GetFileByHotlink(id string) (filestructure.File, bool) {
|
||||
var emptyResult = filestructure.File{}
|
||||
if id == "" {
|
||||
return emptyResult, false
|
||||
}
|
||||
hotlink := configuration.ServerSettings.Hotlinks[id]
|
||||
return GetFile(hotlink.FileId)
|
||||
}
|
||||
|
||||
// Subtracts a download allowance and serves the file to the browser
|
||||
func ServeFile(file filestructure.File, w http.ResponseWriter, r *http.Request, forceDownload bool) {
|
||||
file.DownloadsRemaining = file.DownloadsRemaining - 1
|
||||
configuration.ServerSettings.Files[file.Id] = file
|
||||
// Investigate: Possible race condition with clean-up routine?
|
||||
configuration.Save()
|
||||
|
||||
if forceDownload {
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\""+file.Name+"\"")
|
||||
}
|
||||
w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
|
||||
storageData, err := os.OpenFile(configuration.ServerSettings.DataDir+"/"+file.SHA256, os.O_RDONLY, 0644)
|
||||
defer storageData.Close()
|
||||
helper.Check(err)
|
||||
_, err = io.Copy(w, storageData)
|
||||
helper.Check(err)
|
||||
}
|
||||
|
||||
// Removes expired files from the config and from the filesystem if they are not referenced by other files anymore
|
||||
// Will be called periodically or after a file has been manually deleted in the admin view.
|
||||
// If parameter periodic is true, this function is recursive and calls itself every hour.
|
||||
@@ -75,6 +142,9 @@ func CleanUp(periodic bool) {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
if element.HotlinkId != "" {
|
||||
delete(configuration.ServerSettings.Hotlinks, element.HotlinkId)
|
||||
}
|
||||
delete(configuration.ServerSettings.Files, key)
|
||||
wasItemDeleted = true
|
||||
}
|
||||
|
||||
@@ -15,14 +15,21 @@ type File struct {
|
||||
ExpireAtString string `json:"ExpireAtString"`
|
||||
DownloadsRemaining int `json:"DownloadsRemaining"`
|
||||
PasswordHash string `json:"PasswordHash"`
|
||||
HotlinkId string `json:"HotlinkId"`
|
||||
}
|
||||
|
||||
type Hotlink struct {
|
||||
Id string `json:"Id"`
|
||||
FileId string `json:"FileId"`
|
||||
}
|
||||
|
||||
// Converts the file info to a json String used for returning a result for an upload
|
||||
func (f *File) ToJsonResult(serverUrl string) string {
|
||||
result := Result{
|
||||
Result: "OK",
|
||||
Url: serverUrl + "d?id=",
|
||||
FileInfo: f,
|
||||
Result: "OK",
|
||||
Url: serverUrl + "d?id=",
|
||||
HotlinkUrl: serverUrl + "hotlink/",
|
||||
FileInfo: f,
|
||||
}
|
||||
bytes, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
@@ -34,7 +41,8 @@ func (f *File) ToJsonResult(serverUrl string) string {
|
||||
|
||||
// The struct used for the result after an upload
|
||||
type Result struct {
|
||||
Result string `json:"Result"`
|
||||
FileInfo *File `json:"FileInfo"`
|
||||
Url string `json:"Url"`
|
||||
Result string `json:"Result"`
|
||||
FileInfo *File `json:"FileInfo"`
|
||||
Url string `json:"Url"`
|
||||
HotlinkUrl string `json:"HotlinkUrl"`
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -26,18 +25,28 @@ import (
|
||||
// Variable containing all parsed templates
|
||||
var templateFolder *template.Template
|
||||
|
||||
var imageExpiredPicture []byte
|
||||
|
||||
const expiredFile = "static/expired.png"
|
||||
|
||||
// Starts the webserver on the port set in the config
|
||||
func Start(staticFolderEmbedded, templateFolderEmbedded embed.FS) {
|
||||
initTemplates(templateFolderEmbedded)
|
||||
webserverDir, _ := fs.Sub(staticFolderEmbedded, "static")
|
||||
func Start(staticFolderEmbedded, templateFolderEmbedded *embed.FS) {
|
||||
initTemplates(*templateFolderEmbedded)
|
||||
webserverDir, _ := fs.Sub(*staticFolderEmbedded, "static")
|
||||
var err error
|
||||
if helper.FolderExists("static") {
|
||||
fmt.Println("Found folder 'static', using local folder instead of internal static folder")
|
||||
http.Handle("/", http.FileServer(http.Dir("static")))
|
||||
imageExpiredPicture, err = os.ReadFile(expiredFile)
|
||||
helper.Check(err)
|
||||
} else {
|
||||
http.Handle("/", http.FileServer(http.FS(webserverDir)))
|
||||
imageExpiredPicture, err = fs.ReadFile(staticFolderEmbedded, expiredFile)
|
||||
helper.Check(err)
|
||||
}
|
||||
http.HandleFunc("/index", showIndex)
|
||||
http.HandleFunc("/d", showDownload)
|
||||
http.HandleFunc("/hotlink/", showHotlink)
|
||||
http.HandleFunc("/error", showError)
|
||||
http.HandleFunc("/login", showLogin)
|
||||
http.HandleFunc("/logout", doLogout)
|
||||
@@ -132,19 +141,12 @@ type LoginView struct {
|
||||
// If it exists, a download form is shown or a password needs to be entered.
|
||||
func showDownload(w http.ResponseWriter, r *http.Request) {
|
||||
keyId := queryUrl(w, r, "error")
|
||||
if keyId == "" {
|
||||
return
|
||||
}
|
||||
file := configuration.ServerSettings.Files[keyId]
|
||||
if file.ExpireAt < time.Now().Unix() || file.DownloadsRemaining < 1 {
|
||||
file, ok := storage.GetFile(keyId)
|
||||
if !ok {
|
||||
redirect(w, "error")
|
||||
return
|
||||
}
|
||||
if !helper.FileExists(configuration.ServerSettings.DataDir + "/" + file.SHA256) {
|
||||
redirect(w, "error")
|
||||
return
|
||||
|
||||
}
|
||||
view := DownloadView{
|
||||
Name: file.Name,
|
||||
Size: file.Size,
|
||||
@@ -176,6 +178,20 @@ func showDownload(w http.ResponseWriter, r *http.Request) {
|
||||
helper.Check(err)
|
||||
}
|
||||
|
||||
// Handling of /hotlink/
|
||||
// Hotlinks an image or returns a static error image if image has expired
|
||||
func showHotlink(w http.ResponseWriter, r *http.Request) {
|
||||
hotlinkId := strings.Replace(r.URL.Path, "/hotlink/", "", 1)
|
||||
file, ok := storage.GetFileByHotlink(hotlinkId)
|
||||
if !ok {
|
||||
w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
|
||||
_, err := w.Write(imageExpiredPicture)
|
||||
helper.Check(err)
|
||||
return
|
||||
}
|
||||
storage.ServeFile(file, w, r, false)
|
||||
}
|
||||
|
||||
// Handling of /delete
|
||||
// User needs to be admin. Deleted the requested file
|
||||
func deleteFile(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -227,6 +243,7 @@ type DownloadView struct {
|
||||
type UploadView struct {
|
||||
Items []filestructure.File
|
||||
Url string
|
||||
HotlinkUrl string
|
||||
TimeNow int64
|
||||
DefaultDownloads int
|
||||
DefaultExpiry int
|
||||
@@ -244,6 +261,7 @@ func (u *UploadView) convertGlobalConfig() *UploadView {
|
||||
return result[i].ExpireAt > result[j].ExpireAt
|
||||
})
|
||||
u.Url = configuration.ServerSettings.ServerUrl + "d?id="
|
||||
u.HotlinkUrl = configuration.ServerSettings.ServerUrl + "hotlink/"
|
||||
u.DefaultPassword = configuration.ServerSettings.DefaultPassword
|
||||
u.Items = result
|
||||
u.DefaultExpiry = configuration.ServerSettings.DefaultExpiry
|
||||
@@ -295,11 +313,8 @@ func responseError(w http.ResponseWriter, err error) {
|
||||
// Outputs the file to the user and reduces the download remaining count for the file
|
||||
func downloadFile(w http.ResponseWriter, r *http.Request) {
|
||||
keyId := queryUrl(w, r, "error")
|
||||
if keyId == "" {
|
||||
return
|
||||
}
|
||||
savedFile := configuration.ServerSettings.Files[keyId]
|
||||
if savedFile.DownloadsRemaining == 0 || savedFile.ExpireAt < time.Now().Unix() || !helper.FileExists(configuration.ServerSettings.DataDir+"/"+savedFile.SHA256) {
|
||||
savedFile, ok := storage.GetFile(keyId)
|
||||
if !ok {
|
||||
redirect(w, "error")
|
||||
return
|
||||
}
|
||||
@@ -309,17 +324,7 @@ func downloadFile(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
savedFile.DownloadsRemaining = savedFile.DownloadsRemaining - 1
|
||||
configuration.ServerSettings.Files[keyId] = savedFile
|
||||
configuration.Save()
|
||||
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\""+savedFile.Name+"\"")
|
||||
w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
|
||||
file, err := os.OpenFile(configuration.ServerSettings.DataDir+"/"+savedFile.SHA256, os.O_RDONLY, 0644)
|
||||
defer file.Close()
|
||||
helper.Check(err)
|
||||
_, err = io.Copy(w, file)
|
||||
helper.Check(err)
|
||||
storage.ServeFile(savedFile, w, r, true)
|
||||
}
|
||||
|
||||
// Checks if the user is logged in as an admin
|
||||
|
||||
BIN
static/expired.png
Normal file
BIN
static/expired.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
@@ -57,7 +57,16 @@ function addRow(jsonText) {
|
||||
cell3.innerText = item.DownloadsRemaining;
|
||||
cell4.innerText = item.ExpireAtString;
|
||||
cell5.innerHTML = '<a target="_blank" style="color: inherit" href="'+jsonObject.Url+item.Id+'">'+jsonObject.Url+item.Id+'</a>'+lockIcon;
|
||||
cell6.innerHTML = "<button type=\"button\" data-clipboard-text=\""+jsonObject.Url+item.Id+"\" class=\"copyurl btn btn-outline-light btn-sm\">Copy URL</button> <button type=\"button\" class=\"btn btn-outline-light btn-sm\" onclick=\"window.location='./delete?id="+item.Id+"'\">Delete</button>";
|
||||
|
||||
let buttons = "<button type=\"button\" data-clipboard-text=\""+jsonObject.Url+item.Id+"\" class=\"copyurl btn btn-outline-light btn-sm\">Copy URL</button> ";
|
||||
if (item.HotlinkId != "") {
|
||||
buttons = buttons + '<button type="button" data-clipboard-text="'+jsonObject.HotlinkUrl+item.HotlinkId+'" class="copyurl btn btn-outline-light btn-sm">Copy Hotlink</button> ';
|
||||
} else {
|
||||
buttons = buttons + '<button type="button"class="copyurl btn btn-outline-light btn-sm disabled">Copy Hotlink</button> ';
|
||||
}
|
||||
buttons = buttons + "<button type=\"button\" class=\"btn btn-outline-light btn-sm\" onclick=\"window.location='./delete?id="+item.Id+"'\">Delete</button>";
|
||||
|
||||
cell6.innerHTML = buttons;
|
||||
cell1.style.backgroundColor="green"
|
||||
cell2.style.backgroundColor="green"
|
||||
cell3.style.backgroundColor="green"
|
||||
|
||||
@@ -52,7 +52,12 @@
|
||||
<td scope="col">{{ .DownloadsRemaining }}</td>
|
||||
<td scope="col">{{ .ExpireAtString }}</td>
|
||||
<td scope="col"><a target="_blank" href="{{ $.Url }}{{ .Id }}">{{ $.Url }}{{ .Id }}</a>{{ if ne .PasswordHash "" }} 🔒{{ end }}</td>
|
||||
<td scope="col"><button type="button" data-clipboard-text="{{ $.Url }}{{ .Id }}" class="copyurl btn btn-outline-light btn-sm">Copy URL</button> <button type="button" class="btn btn-outline-light btn-sm" onclick="window.location='./delete?id={{ .Id }}'">Delete</button></td>
|
||||
<td scope="col"><button type="button" data-clipboard-text="{{ $.Url }}{{ .Id }}" class="copyurl btn btn-outline-light btn-sm">Copy URL</button>
|
||||
{{ if ne .HotlinkId "" }}
|
||||
<button type="button" data-clipboard-text="{{ $.HotlinkUrl }}{{ .HotlinkId }}" class="copyurl btn btn-outline-light btn-sm">Copy Hotlink</button>
|
||||
{{ else }}
|
||||
<button type="button"class="copyurl btn btn-outline-light btn-sm disabled">Copy Hotlink</button>
|
||||
{{ end }} <button type="button" class="btn btn-outline-light btn-sm" onclick="window.location='./delete?id={{ .Id }}'">Delete</button></td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
{{define "app_name"}}Gokapi{{end}}
|
||||
{{define "version"}}1.1.2{{end}}
|
||||
{{define "version"}}1.1.3-dev{{end}}
|
||||
|
||||
Reference in New Issue
Block a user