Subsonic: sharing (getShares, createShare, updateShare, deleteShare) plus real public share web pages and stream route
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.noroutes.net/NoRoutes/vibetunes/internal/database"
|
||||
"git.noroutes.net/NoRoutes/vibetunes/internal/streaming"
|
||||
)
|
||||
|
||||
// handleSharePage serves a public, unauthenticated page for a share
|
||||
// token — the whole point of a share link is that the recipient doesn't
|
||||
// need a VibeSonic account. Expired or unknown tokens 404 rather than
|
||||
// leaking which is which.
|
||||
func (h *Web) handleSharePage(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.PathValue("token")
|
||||
share, err := database.GetShareByToken(h.DB, token)
|
||||
if err != nil || share.IsExpired() {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
_ = database.RecordShareVisit(h.DB, share.ID)
|
||||
|
||||
songIDs, err := database.ShareSongIDs(h.DB, share.ID)
|
||||
if err != nil {
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var rows []SongRow
|
||||
for _, id := range songIDs {
|
||||
song, serr := database.GetSong(h.DB, id)
|
||||
if serr != nil {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, toSongRow(h.DB, song, albumOf(h.DB, song), ""))
|
||||
}
|
||||
|
||||
owner, _ := database.GetUserByID(h.DB, share.OwnerID)
|
||||
ownerName := ""
|
||||
if owner != nil {
|
||||
ownerName = owner.Username
|
||||
}
|
||||
|
||||
render(w, "share", PageData{
|
||||
Title: "Shared music",
|
||||
Data: struct {
|
||||
Description string
|
||||
OwnerName string
|
||||
Songs []SongRow
|
||||
Token string
|
||||
}{share.Description, ownerName, rows, token},
|
||||
})
|
||||
}
|
||||
|
||||
// handleShareStream serves one song from a share, publicly and without
|
||||
// a session — but only a song that is actually part of that share, and
|
||||
// only through the same path-validated streaming.ServeFile used
|
||||
// everywhere else, so a share link can never be used to fetch arbitrary
|
||||
// filesystem paths.
|
||||
func (h *Web) handleShareStream(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.PathValue("token")
|
||||
songID := r.PathValue("songID")
|
||||
|
||||
share, err := database.GetShareByToken(h.DB, token)
|
||||
if err != nil || share.IsExpired() {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
songIDs, err := database.ShareSongIDs(h.DB, share.ID)
|
||||
if err != nil {
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
allowed := false
|
||||
for _, id := range songIDs {
|
||||
if id == songID {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
song, err := database.GetSong(h.DB, songID)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
mf, err := database.GetMediaFile(h.DB, song.MediaFileID)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
folder, err := database.GetMusicFolder(h.DB, mf.MusicFolderID)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
absPath, perr := streaming.ResolvePath(folder.Path, mf.RelativePath)
|
||||
if perr != nil {
|
||||
http.Error(w, "403 Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if err := streaming.ServeFile(w, r, absPath, mf.RelativePath); err != nil {
|
||||
http.Error(w, "could not read media file", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user