Initial commit

Proof-of-concept implementation. Bugs will occur.
This commit is contained in:
2026-02-12 01:18:46 +03:00
commit 13ac06c14b
553 changed files with 253003 additions and 0 deletions

View File

@@ -0,0 +1,287 @@
package filesystem
import (
"context"
"os"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/sirupsen/logrus"
)
// Any non-root directory is a MusicDirectory
type MusicDir struct {
fs.Inode
f *FS
path string
}
var (
_ = (fs.NodeGetattrer)((*MusicDir)(nil))
_ = (fs.NodeLookuper)((*MusicDir)(nil))
_ = (fs.NodeReaddirer)((*MusicDir)(nil))
_ = (fs.NodeCreater)((*MusicDir)(nil))
_ = (fs.NodeGetxattrer)((*MusicDir)(nil))
_ = (fs.NodeSetxattrer)((*MusicDir)(nil))
_ = (fs.NodeRemovexattrer)((*MusicDir)(nil))
_ = (fs.NodeListxattrer)((*MusicDir)(nil))
)
func (d *MusicDir) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
out.Mode = fuse.S_IFDIR | 0755
out.Nlink = 2 // Minimum . and ..
out.Ino = d.StableAttr().Ino
out.Size = 4096
// Get actual mod time from filesystem if possible
if info, err := os.Stat(d.path); err == nil {
out.Mtime = uint64(info.ModTime().Unix())
out.Atime = out.Mtime
out.Ctime = out.Mtime
// Count actual subdirectories for accurate nlink
if entries, err := os.ReadDir(d.path); err == nil {
for _, entry := range entries {
if entry.IsDir() {
out.Nlink++
}
}
}
} else {
now := uint64(time.Now().Unix())
out.Mtime = now
out.Atime = now
out.Ctime = now
}
out.Blocks = 1
out.Blksize = 512
return 0
}
func (d *MusicDir) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, syscall.Errno) {
// Same implementation as RootDir
switch attr {
case "user.org.netatalk.Metadata":
fallthrough
case "com.apple.FinderInfo":
fallthrough
case "com.apple.ResourceFork":
if len(dest) > 0 {
return 0, 0
}
return 0, 0
default:
return 0, syscall.ENODATA
}
}
func (d *MusicDir) Setxattr(ctx context.Context, attr string, data []byte, flags uint32) syscall.Errno {
return 0
}
func (d *MusicDir) Removexattr(ctx context.Context, attr string) syscall.Errno {
return 0
}
func (d *MusicDir) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errno) {
return 0, 0
}
func (d *MusicDir) Create(ctx context.Context, name string, flags uint32, mode uint32, out *fuse.EntryOut) (*fs.Inode, fs.FileHandle, uint32, syscall.Errno) {
if d.f.isiTunesMetadata(name) {
metaPath := filepath.Join(d.f.metadataDir, name)
file, err := os.Create(metaPath)
if err != nil {
return nil, nil, 0, syscall.EIO
}
ch := d.NewInode(
ctx,
d.f.NewMusicAppMetadataFile(metaPath),
fs.StableAttr{
Mode: fuse.S_IFREG,
Ino: d.f.nextInode(),
},
)
out.Mode = fuse.S_IFREG | 0644
out.Nlink = 1
out.Ino = ch.StableAttr().Ino
out.Size = 0
out.Mtime = uint64(time.Now().Unix())
out.Atime = out.Mtime
out.Ctime = out.Mtime
out.Blocks = 1
return ch, &File{file: file}, fuse.FOPEN_DIRECT_IO, 0
}
return nil, nil, 0, syscall.EPERM
}
func (d *MusicDir) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
// Handle .m4a virtual files
if strings.HasSuffix(strings.ToLower(name), ".m4a") {
flacName := name[:len(name)-4] + ".flac"
flacPath := filepath.Join(d.path, flacName)
if _, err := os.Stat(flacPath); err == nil {
ch := d.NewInode(
ctx,
d.f.NewMusicFile(flacPath, name, false),
fs.StableAttr{
Mode: fuse.S_IFREG,
Ino: d.f.nextInode(),
},
)
out.Mode = fuse.S_IFREG | 0444
out.Nlink = 1
out.Ino = ch.StableAttr().Ino
if size, err := d.f.cacher.GetStat(flacPath); err == nil {
out.Size = uint64(size)
} else {
out.Size = 0
}
out.Mtime = uint64(time.Now().Unix())
out.Atime = out.Mtime
out.Ctime = out.Mtime
out.Blocks = (out.Size + 511) / 512
return ch, 0
}
}
// Check real file or directory
fullPath := filepath.Join(d.path, name)
info, err := os.Stat(fullPath)
if err != nil {
return nil, syscall.ENOENT
}
if info.IsDir() {
ch := d.NewInode(
ctx, d.f.NewMusicDirectory(fullPath),
fs.StableAttr{
Mode: fuse.S_IFDIR,
Ino: d.f.nextInode(),
},
)
out.Mode = fuse.S_IFDIR | 0755
out.Nlink = 2
out.Ino = ch.StableAttr().Ino
out.Size = 4096
out.Mtime = uint64(info.ModTime().Unix())
out.Atime = out.Mtime
out.Ctime = out.Mtime
out.Blocks = 1
return ch, 0
}
// Regular file (non-FLAC)
isMeta := d.f.isiTunesMetadata(name)
ch := d.NewInode(ctx, d.f.NewMusicFile(fullPath, name, isMeta),
fs.StableAttr{
Mode: fuse.S_IFREG,
Ino: d.f.nextInode(),
},
)
if isMeta {
out.Mode = fuse.S_IFREG | 0644
} else {
out.Mode = fuse.S_IFREG | 0444
}
out.Nlink = 1
out.Ino = ch.StableAttr().Ino
out.Size = uint64(info.Size())
out.Mtime = uint64(info.ModTime().Unix())
out.Atime = out.Mtime
out.Ctime = out.Mtime
out.Blocks = (out.Size + 511) / 512
return ch, 0
}
func (d *MusicDir) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) {
d.f.app.Logger().WithField("path", d.path).Debug("Readdir called on directory")
var dirEntries []fuse.DirEntry
dirEntries = append(dirEntries, fuse.DirEntry{
Name: ".",
Mode: fuse.S_IFDIR | 0755,
Ino: d.StableAttr().Ino,
})
dirEntries = append(dirEntries, fuse.DirEntry{
Name: "..",
Mode: fuse.S_IFDIR | 0755,
Ino: 1, // Parent (root) inode
})
entries, err := os.ReadDir(d.path)
if err != nil {
d.f.app.Logger().WithError(err).WithField("path", d.path).Error(
"Error reading directory",
)
return fs.NewListDirStream(dirEntries), 0
}
for _, entry := range entries {
name := entry.Name()
if strings.HasPrefix(name, ".") && !d.f.isiTunesMetadata(name) {
continue
}
mode := fuse.S_IFREG | 0444
if entry.IsDir() {
mode = fuse.S_IFDIR | 0755
}
// Convert .flac to .m4a in directory listing
if strings.HasSuffix(strings.ToLower(name), ".flac") {
name = name[:len(name)-5] + ".m4a"
if !d.f.isiTunesMetadata(name) {
mode = fuse.S_IFREG | 0644
}
} else if !d.f.isiTunesMetadata(name) {
mode = fuse.S_IFREG | 0644
}
dirEntries = append(dirEntries, fuse.DirEntry{
Name: name,
Mode: uint32(mode),
Ino: d.f.nextInode(),
})
}
d.f.app.Logger().WithFields(logrus.Fields{
"path": d.path,
"directory entries": len(dirEntries),
}).Debug("Returning directory entries")
return fs.NewListDirStream(dirEntries), 0
}
func (f *FS) NewMusicDirectory(path string) *MusicDir {
return &MusicDir{
f: f,
path: path,
}
}