diff --git a/dlna/dms/cds.go b/dlna/dms/cds.go index 72abf5b..7a284a2 100644 --- a/dlna/dms/cds.go +++ b/dlna/dms/cds.go @@ -4,12 +4,12 @@ import ( "encoding/json" "encoding/xml" "fmt" + "io/fs" "io/ioutil" "net/http" "net/url" "os" "path" - "path/filepath" "sort" "strconv" "strings" @@ -73,7 +73,7 @@ func readDynamicStream(metadataPath string) (*dmsDynamicMediaItem, error) { return &re, nil } -func (me *contentDirectoryService) cdsObjectDynamicStreamToUpnpavObject(cdsObject object, fileInfo os.FileInfo, host, userAgent string) (ret interface{}, err error) { +func (me *contentDirectoryService) cdsObjectDynamicStreamToUpnpavObject(cdsObject object, fileInfo fs.FileInfo, host, userAgent string) (ret interface{}, err error) { // at this point we know that entryFilePath points to a .dms.json file; slurp and parse dmsMediaItem, err := readDynamicStream(cdsObject.FilePath()) if err != nil { @@ -100,12 +100,12 @@ func (me *contentDirectoryService) cdsObjectDynamicStreamToUpnpavObject(cdsObjec obj.AlbumArtURI = iconURI switch dmsMediaItem.Type { - case "video": - obj.Class = "object.item.videoItem" - case "audio": - obj.Class = "object.item.audioItem" - default: - obj.Class = "object.item.videoItem" + case "video": + obj.Class = "object.item.videoItem" + case "audio": + obj.Class = "object.item.audioItem" + default: + obj.Class = "object.item.videoItem" } obj.Title = dmsMediaItem.Title @@ -170,7 +170,7 @@ func (me *contentDirectoryService) cdsObjectDynamicStreamToUpnpavObject(cdsObjec // returned if the entry is not of interest. func (me *contentDirectoryService) cdsObjectToUpnpavObject( cdsObject object, - fileInfo os.FileInfo, + fileInfo fs.FileInfo, host, userAgent string, ) (ret interface{}, err error) { entryFilePath := cdsObject.FilePath() @@ -204,7 +204,7 @@ func (me *contentDirectoryService) cdsObjectToUpnpavObject( me.Logger.Printf("%s ignored: non-regular file", cdsObject.FilePath()) return } - mimeType, err := MimeTypeByPath(entryFilePath) + mimeType, err := MimeTypeByPath(me.FS, entryFilePath) if err != nil { return } @@ -332,7 +332,7 @@ func (me *contentDirectoryService) readContainer( // TODO(anacrolix): Dig up why this special cast was added. FoldersLast: strings.Contains(userAgent, `AwoX/1.1`), } - sfis.fileInfoSlice, err = o.readDir() + sfis.fileInfoSlice, err = o.readDir(me.FS) if err != nil { return } @@ -366,13 +366,9 @@ func (me *contentDirectoryService) objectFromID(id string) (o object, err error) return } if o.Path == "0" { - o.Path = "/" + o.Path = "." } o.Path = path.Clean(o.Path) - if !path.IsAbs(o.Path) { - err = fmt.Errorf("bad ObjectID %v", o.Path) - return - } o.RootObjectPath = me.RootObjectPath return } @@ -434,8 +430,8 @@ func (me *contentDirectoryService) Handle(action string, argsXML []byte, r *http var ret interface{} var err error if me.OnBrowseMetadata == nil { - var fileInfo os.FileInfo - fileInfo, err = os.Stat(obj.FilePath()) + var fileInfo fs.FileInfo + fileInfo, err = fs.Stat(me.FS, obj.FilePath()) if err != nil { if os.IsNotExist(err) { return nil, &upnp.Error{ @@ -502,7 +498,7 @@ type object struct { func (me *contentDirectoryService) isOfInterest( cdsObject object, - fileInfo os.FileInfo, + fileInfo fs.FileInfo, ) (ret bool, err error) { entryFilePath := cdsObject.FilePath() ignored, err := me.IgnorePath(entryFilePath) @@ -526,7 +522,7 @@ func (me *contentDirectoryService) isOfInterest( return } - mimeType, err := MimeTypeByPath(entryFilePath) + mimeType, err := MimeTypeByPath(me.FS, entryFilePath) if err != nil { return } @@ -539,7 +535,7 @@ func (me *contentDirectoryService) isOfInterest( // Returns the number of children this object has, such as for a container. func (cds *contentDirectoryService) objectChildCount(me object) (count int) { - fileInfoSlice, err := me.readDir() + fileInfoSlice, err := me.readDir(cds.FS) if err != nil { return } @@ -562,13 +558,13 @@ func (cds *contentDirectoryService) objectChildCount(me object) (count int) { // directory succeeds. Returns true on first hit. func (me *contentDirectoryService) objectHasChildren( cdsObject object, - fileInfo os.FileInfo, + fileInfo fs.FileInfo, ) (ret bool, err error) { if !fileInfo.IsDir() { panic("Expected directory") } - files, err := cdsObject.readDir() + files, err := cdsObject.readDir(me.FS) if err != nil { return } @@ -588,14 +584,11 @@ func (me *contentDirectoryService) objectHasChildren( // Returns the actual local filesystem path for the object. func (o *object) FilePath() string { - return filepath.Join(o.RootObjectPath, filepath.FromSlash(o.Path)) + return path.Join(o.RootObjectPath, path.Clean(o.Path)) } // Returns the ObjectID for the object. This is used in various ContentDirectory actions. func (o object) ID() string { - if !path.IsAbs(o.Path) { - log.Panicf("Relative object path: %s", o.Path) - } if len(o.Path) == 1 { return "0" } @@ -603,7 +596,7 @@ func (o object) ID() string { } func (o *object) IsRoot() bool { - return o.Path == "/" + return o.Path == "." } // Returns the object's parent ObjectID. Fortunately it can be deduced from the @@ -618,31 +611,21 @@ func (o object) ParentID() string { // This function exists rather than just calling os.(*File).Readdir because I // want to stat(), not lstat() each entry. -func (o *object) readDir() (fis []os.FileInfo, err error) { - dirPath := o.FilePath() - dirFile, err := os.Open(dirPath) +func (o *object) readDir(fsys fs.FS) (fis []fs.FileInfo, err error) { + dirFile, err := fs.ReadDir(fsys, o.Path) if err != nil { return } - defer dirFile.Close() - var dirContent []string - dirContent, err = dirFile.Readdirnames(-1) - if err != nil { - return - } - fis = make([]os.FileInfo, 0, len(dirContent)) - for _, file := range dirContent { - fi, err := os.Stat(filepath.Join(dirPath, file)) - if err != nil { - continue - } + fis = make([]fs.FileInfo, 0, len(dirFile)) + for _, file := range dirFile { + fi, _ := file.Info() fis = append(fis, fi) } return } type sortableFileInfoSlice struct { - fileInfoSlice []os.FileInfo + fileInfoSlice []fs.FileInfo FoldersLast bool } diff --git a/dlna/dms/dms.go b/dlna/dms/dms.go index 26db29e..e9023ae 100644 --- a/dlna/dms/dms.go +++ b/dlna/dms/dms.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "io/fs" "io/ioutil" "math/rand" "net" @@ -280,6 +281,7 @@ type Server struct { TranscodeLogPattern string Logger log.Logger eventingLogger log.Logger + FS fs.FS } // UPnP SOAP service. @@ -638,7 +640,7 @@ func (me *Server) serviceControlHandler(w http.ResponseWriter, r *http.Request) } func safeFilePath(root, given string) string { - return filepath.Join(root, filepath.FromSlash(path.Clean("/" + given))[1:]) + return path.Join(root, path.Clean(given)) } func (s *Server) filePath(_path string) string { @@ -864,7 +866,7 @@ func (server *Server) initMux(mux *http.ServeMux) { } else { k = r.URL.Query().Get("transcode") } - mimeType, err := MimeTypeByPath(filePath) + mimeType, err := MimeTypeByPath(server.FS, filePath) if k == "" || mimeType.IsImage() { if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -872,7 +874,7 @@ func (server *Server) initMux(mux *http.ServeMux) { } w.Header().Set("Content-Type", string(mimeType)) w.Header().Set("Content-Disposition", "attachment; filename="+strconv.Quote(path.Base(filePath))) - http.ServeFile(w, r, filePath) + http.ServeFileFS(w, r, server.FS, filePath) return } if server.NoTranscode { @@ -939,6 +941,11 @@ func (s *Server) initServices() (err error) { } func (srv *Server) Init() (err error) { + if srv.FS == nil { + fsys := os.DirFS(srv.RootObjectPath) + srv.FS = fsys + } + srv.RootObjectPath = "." srv.eventingLogger = srv.Logger.WithNames("eventing") srv.eventingLogger.Levelf(log.Debug, "hello %v", "world") if err = srv.initServices(); err != nil { @@ -1071,19 +1078,15 @@ func (me *Server) location(ip net.IP) string { // Can return nil info with nil err if an earlier Probe gave an error. func (srv *Server) ffmpegProbe(path string) (info *ffprobe.Info, err error) { - // We don't want relative paths in the cache. - path, err = filepath.Abs(path) - if err != nil { - return - } - fi, err := os.Stat(path) + fi, err := fs.Stat(srv.FS, path) if err != nil { return } key := ffmpegInfoCacheKey{path, fi.ModTime().UnixNano()} value, ok := srv.FFProbeCache.Get(key) if !ok { - info, err = ffprobe.Run(path) + uri := fmt.Sprintf("http://127.0.0.1:%d%s?path=%s", srv.httpPort(), resPath, path) + info, err = ffprobe.Run(uri) err = suppressFFmpegProbeDataErrors(err) srv.FFProbeCache.Set(key, info) return @@ -1094,11 +1097,8 @@ func (srv *Server) ffmpegProbe(path string) (info *ffprobe.Info, err error) { // IgnorePath detects if a file/directory should be ignored. func (server *Server) IgnorePath(path string) (bool, error) { - if !filepath.IsAbs(path) { - return false, fmt.Errorf("Path must be absolute: %s", path) - } if server.IgnoreHidden { - if hidden, err := isHiddenPath(path); err != nil { + if hidden, err := isHiddenPath(server.FS, path); err != nil { return false, err } else if hidden { log.Print(path, " ignored: hidden") @@ -1106,7 +1106,7 @@ func (server *Server) IgnorePath(path string) (bool, error) { } } if server.IgnoreUnreadable { - if readable, err := isReadablePath(path); err != nil { + if readable, err := isReadablePath(server.FS, path); err != nil { return false, err } else if !readable { log.Print(path, " ignored: unreadable") @@ -1124,13 +1124,12 @@ func (server *Server) IgnorePath(path string) (bool, error) { return false, nil } -func tryToOpenPath(path string) (bool, error) { +func isReadablePath(fsys fs.FS, path string) (bool, error) { // Ugly but portable way to check if we can open a file/directory - if fh, err := os.Open(path); err == nil { - fh.Close() - return true, nil - } else if !os.IsPermission(err) { + f, err := fsys.Open(path) + if err != nil { return false, err } - return false, nil + f.Close() + return true, nil } diff --git a/dlna/dms/dms_unix.go b/dlna/dms/dms_unix.go index 67f0df0..b489023 100644 --- a/dlna/dms/dms_unix.go +++ b/dlna/dms/dms_unix.go @@ -4,23 +4,19 @@ package dms import ( + "io/fs" + "path/filepath" "strings" - - "golang.org/x/sys/unix" ) -func isHiddenPath(path string) (bool, error) { - return strings.Contains(path, "/."), nil -} - -func isReadablePath(path string) (bool, error) { - err := unix.Access(path, unix.R_OK) - switch err { - case nil: - return true, nil - case unix.EACCES: +func isHiddenPath(fsys fs.FS, path string) (bool, error) { + if path == "." { return false, nil - default: - return false, err } + base := filepath.Base(path) + if strings.HasPrefix(base, ".") { + return true, nil + } + + return isHiddenPath(fsys, filepath.Dir(path)) } diff --git a/dlna/dms/dms_unix_test.go b/dlna/dms/dms_unix_test.go index cc16ba3..9c6fc06 100644 --- a/dlna/dms/dms_unix_test.go +++ b/dlna/dms/dms_unix_test.go @@ -7,17 +7,17 @@ import "testing" func TestIsHiddenPath(t *testing.T) { data := map[string]bool{ - "/some/path": false, - "/some/foo.bar": false, - "/some/path/.hidden": true, - "/some/.hidden/path": true, - "/.hidden/path": true, + "some/path": false, + "some/foo.bar": false, + "some/path/.hidden": true, + "some/.hidden/path": true, + ".hidden/path": true, } for path, expected := range data { - if actual, err := isHiddenPath(path); err != nil { - t.Errorf("isHiddenPath(%v) returned unexpected error: %s", path, err) + if actual, err := isHiddenPath(nil, path); err != nil { + t.Errorf("isHiddenPath(nil, %v) returned unexpected error: %s", path, err) } else if expected != actual { - t.Errorf("isHiddenPath(%v), expected %v, got %v", path, expected, actual) + t.Errorf("isHiddenPath(nil, %v), expected %v, got %v", path, expected, actual) } } } diff --git a/dlna/dms/dms_windows.go b/dlna/dms/dms_windows.go index 14db0e4..007f54d 100644 --- a/dlna/dms/dms_windows.go +++ b/dlna/dms/dms_windows.go @@ -4,33 +4,36 @@ package dms import ( + "io/fs" "path/filepath" + "syscall" "golang.org/x/sys/windows" ) const hiddenAttributes = windows.FILE_ATTRIBUTE_HIDDEN | windows.FILE_ATTRIBUTE_SYSTEM -func isHiddenPath(path string) (hidden bool, err error) { - if path == filepath.VolumeName(path)+"\\" { - // Volumes always have the "SYSTEM" flag, so do not even test them +func isHiddenPath(fsys *fs.FS, path string) (hidden bool, err error) { + if path == "." { return false, nil } - winPath, err := windows.UTF16PtrFromString(path) + f, err := (*fsys).Open(path) if err != nil { - return + return false, err } - attrs, err := windows.GetFileAttributes(winPath) + defer f.Close() + fi, err := f.Stat() if err != nil { - return + return false, err } - if attrs&hiddenAttributes != 0 { - hidden = true - return + // Extract the Win32FileAttributeData from Sys() + sys, ok := fi.Sys().(*syscall.Win32FileAttributeData) + if !ok { + return false, nil // Not a Windows file system? Default to non-hidden. + } + if (sys.FileAttributes & hiddenAttributes) != 0 { + return true, nil } - return isHiddenPath(filepath.Dir(path)) -} -func isReadablePath(path string) (bool, error) { - return tryToOpenPath(path) + return isHiddenPath(fsys, filepath.ToSlash(filepath.Dir(path))) } diff --git a/dlna/dms/mimetype.go b/dlna/dms/mimetype.go index 358cf33..d9ca846 100644 --- a/dlna/dms/mimetype.go +++ b/dlna/dms/mimetype.go @@ -1,9 +1,9 @@ package dms import ( + "io/fs" "mime" "net/http" - "os" "path" "strings" @@ -56,10 +56,10 @@ func (mt mimeType) String() string { } // MimeTypeByPath determines the MIME-type of file at the given path -func MimeTypeByPath(filePath string) (ret mimeType, err error) { +func MimeTypeByPath(fsys fs.FS, filePath string) (ret mimeType, err error) { ret = mimeTypeByBaseName(path.Base(filePath)) if ret == "" { - ret, err = mimeTypeByContent(filePath) + ret, err = mimeTypeByContent(fsys, filePath) } if ret == "video/x-msvideo" { ret = "video/avi" @@ -80,8 +80,8 @@ func mimeTypeByBaseName(name string) mimeType { } // Guess the MIME-type by analysing the first 512 bytes of the file. -func mimeTypeByContent(path string) (ret mimeType, err error) { - file, err := os.Open(path) +func mimeTypeByContent(fsys fs.FS, path string) (ret mimeType, err error) { + file, err := fsys.Open(path) if err != nil { return }