247 lines
6.5 KiB
Go
247 lines
6.5 KiB
Go
package zfs
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"bazil.org/fuse"
|
|
"bazil.org/fuse/fs"
|
|
"github.com/debridmediamanager.com/zurg/pkg/realdebrid"
|
|
)
|
|
|
|
// define variable as rootObject id
|
|
const (
|
|
ROOT = 0
|
|
DIRECTORY = 1
|
|
TORRENT = 2
|
|
FILE = 3
|
|
)
|
|
|
|
type Object struct {
|
|
fs *FS
|
|
objType int
|
|
parentName string
|
|
name string
|
|
link string
|
|
size uint64
|
|
mtime time.Time
|
|
}
|
|
|
|
// Attr returns the attributes for a directory
|
|
func (o Object) Attr(ctx context.Context, attr *fuse.Attr) error {
|
|
if o.objType == FILE {
|
|
attr.Mode = 0644
|
|
} else {
|
|
attr.Mode = os.ModeDir | 0755
|
|
}
|
|
attr.Size = o.size
|
|
|
|
attr.Uid = o.fs.uid
|
|
attr.Gid = o.fs.gid
|
|
|
|
attr.Ctime = o.mtime
|
|
attr.Mtime = o.mtime
|
|
|
|
attr.Blocks = (attr.Size + 511) >> 9
|
|
|
|
return nil
|
|
}
|
|
|
|
// ReadDirAll shows all files in the current directory
|
|
func (o Object) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
|
|
dirs := []fuse.Dirent{}
|
|
switch o.objType {
|
|
case ROOT:
|
|
for _, directory := range o.fs.c.GetDirectories() {
|
|
dirs = append(dirs, fuse.Dirent{
|
|
Name: directory,
|
|
Type: fuse.DT_Dir,
|
|
})
|
|
}
|
|
case DIRECTORY:
|
|
seen := make(map[string]bool)
|
|
for _, item := range o.fs.t.GetByDirectory(o.name) {
|
|
if item.Progress != 100 {
|
|
continue
|
|
}
|
|
if _, exists := seen[item.Name]; exists {
|
|
continue
|
|
}
|
|
seen[item.Name] = true
|
|
dirs = append(dirs, fuse.Dirent{
|
|
Name: item.Name,
|
|
Type: fuse.DT_Dir,
|
|
})
|
|
}
|
|
case TORRENT:
|
|
finalName := make(map[string]bool)
|
|
for _, item := range o.fs.t.FindAllTorrentsWithName(o.parentName, o.name) {
|
|
for _, file := range item.SelectedFiles {
|
|
if file.Link == "" {
|
|
// log.Println("File has no link, skipping", file.Path)
|
|
continue
|
|
}
|
|
filename := filepath.Base(file.Path)
|
|
if finalName[filename] {
|
|
// fragment := davextra.GetLinkFragment(file.Link)
|
|
// filename = davextra.InsertLinkFragment(filename, fragment)
|
|
continue
|
|
}
|
|
finalName[filename] = true
|
|
dirs = append(dirs, fuse.Dirent{
|
|
Name: filename,
|
|
Type: fuse.DT_File,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return dirs, nil
|
|
}
|
|
|
|
// Lookup tests if a file is existent in the current directory
|
|
func (o Object) Lookup(ctx context.Context, name string) (fs.Node, error) {
|
|
switch o.objType {
|
|
case ROOT:
|
|
for _, directory := range o.fs.c.GetDirectories() {
|
|
if directory == name {
|
|
return Object{
|
|
fs: o.fs,
|
|
objType: DIRECTORY,
|
|
parentName: o.name,
|
|
name: name,
|
|
mtime: o.fs.initTime,
|
|
}, nil
|
|
}
|
|
}
|
|
case DIRECTORY:
|
|
for _, item := range o.fs.t.GetByDirectory(o.name) {
|
|
if item.Name == name && item.Progress == 100 {
|
|
return Object{
|
|
fs: o.fs,
|
|
objType: TORRENT,
|
|
parentName: o.name,
|
|
name: name,
|
|
mtime: convertRFC3339toTime(item.Added),
|
|
}, nil
|
|
}
|
|
}
|
|
case TORRENT:
|
|
for _, item := range o.fs.t.FindAllTorrentsWithName(o.parentName, o.name) {
|
|
for _, file := range item.SelectedFiles {
|
|
if strings.HasSuffix(file.Path, name) && file.Link != "" {
|
|
return Object{
|
|
fs: o.fs,
|
|
objType: FILE,
|
|
parentName: o.name,
|
|
name: name,
|
|
link: file.Link,
|
|
size: uint64(file.Bytes),
|
|
mtime: convertRFC3339toTime(item.Added),
|
|
}, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil, syscall.ENOENT
|
|
}
|
|
|
|
// Open a file
|
|
func (o Object) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.OpenResponse) (fs.Handle, error) {
|
|
resp.Flags |= fuse.OpenDirectIO
|
|
return o, nil
|
|
}
|
|
|
|
// Read reads some bytes or the whole file
|
|
func (o Object) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error {
|
|
o.fs.log.Debugf("Reading offset %d size %d", req.Offset, req.Size)
|
|
unrestrictFn := func() (*realdebrid.UnrestrictResponse, error) {
|
|
return realdebrid.UnrestrictLink(o.fs.c.GetToken(), o.link)
|
|
}
|
|
unrestrictResp := realdebrid.RetryUntilOk(unrestrictFn)
|
|
if unrestrictResp == nil {
|
|
o.fs.log.Errorf("Cannot unrestrict link %s", o.link)
|
|
return syscall.EIO
|
|
} else if unrestrictResp.Filename != o.name {
|
|
actualExt := filepath.Ext(unrestrictResp.Filename)
|
|
expectedExt := filepath.Ext(o.name)
|
|
if actualExt != expectedExt {
|
|
o.fs.log.Errorf("File extension mismatch: expected %s, got %s", expectedExt, actualExt)
|
|
} else {
|
|
o.fs.log.Errorf("Filename mismatch: expected %s, got %s", o.name, unrestrictResp.Filename)
|
|
}
|
|
}
|
|
o.fs.log.Debugf("Unrestricted link: %s", unrestrictResp.Download)
|
|
|
|
httpClient := &http.Client{
|
|
Timeout: 30 * time.Second, // Set a timeout to prevent hanging
|
|
}
|
|
|
|
streamReq, err := http.NewRequest(http.MethodGet, unrestrictResp.Download, nil)
|
|
if err != nil {
|
|
o.fs.log.Errorf("Error creating new request %v", err)
|
|
return syscall.EIO
|
|
}
|
|
|
|
// Ensure that the byte range is correctly specified
|
|
endByte := req.Offset + int64(req.Size) - 1
|
|
streamReq.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", req.Offset, endByte))
|
|
o.fs.log.Debugf("Requested bytes=%d-%d", req.Offset, endByte)
|
|
|
|
streamResp, err := httpClient.Do(streamReq.WithContext(ctx))
|
|
if err != nil {
|
|
o.fs.log.Errorf("Error downloading file %s %v", unrestrictResp.Download, err)
|
|
return syscall.EIO
|
|
}
|
|
defer streamResp.Body.Close()
|
|
|
|
// Check for successful status codes
|
|
if streamResp.StatusCode != http.StatusOK && streamResp.StatusCode != http.StatusPartialContent {
|
|
o.fs.log.Errorf("Received a non-ok status code %d for %s", streamResp.StatusCode, unrestrictResp.Download)
|
|
return syscall.EIO
|
|
}
|
|
|
|
// Optionally check if the server returned the expected byte range
|
|
contentRange := streamResp.Header.Get("Content-Range")
|
|
if contentRange == "" && streamResp.StatusCode == http.StatusPartialContent {
|
|
o.fs.log.Error("Content-Range header is missing in HTTP partial content response")
|
|
return syscall.EIO
|
|
}
|
|
|
|
// Process the stream in chunks.
|
|
// Allocate a buffer to read into.
|
|
respData, readErr := io.ReadAll(streamResp.Body)
|
|
if readErr != nil {
|
|
o.fs.log.Errorf("Error reading bytes from download response %v", readErr)
|
|
return syscall.EIO
|
|
}
|
|
resp.Data = respData
|
|
o.fs.log.Debugf("Read %d bytes in total", len(resp.Data))
|
|
return nil
|
|
}
|
|
|
|
// Remove deletes an element
|
|
func (o Object) Remove(ctx context.Context, req *fuse.RemoveRequest) error {
|
|
return fmt.Errorf("Remove not yet implemented")
|
|
}
|
|
|
|
// Rename renames an element
|
|
func (o Object) Rename(ctx context.Context, req *fuse.RenameRequest, newDir fs.Node) error {
|
|
return fmt.Errorf("Rename not yet implemented")
|
|
}
|
|
|
|
func convertRFC3339toTime(input string) time.Time {
|
|
layout := "2006-01-02T15:04:05.000Z"
|
|
t, err := time.Parse(layout, input)
|
|
if err != nil {
|
|
return time.Now()
|
|
}
|
|
return t
|
|
}
|