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 }