2013-10-06 15:01:39 +08:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bazil.org/fuse"
|
|
|
|
fusefs "bazil.org/fuse/fs"
|
|
|
|
"bitbucket.org/anacrolix/go.torrent"
|
|
|
|
"flag"
|
2013-10-13 20:16:21 +08:00
|
|
|
"github.com/davecheney/profile"
|
2013-10-06 15:01:39 +08:00
|
|
|
metainfo "github.com/nsf/libtorgo/torrent"
|
|
|
|
"log"
|
2013-10-14 22:39:12 +08:00
|
|
|
"net"
|
2013-10-13 20:16:21 +08:00
|
|
|
"net/http"
|
|
|
|
_ "net/http/pprof"
|
2013-10-06 15:01:39 +08:00
|
|
|
"os"
|
|
|
|
"os/user"
|
|
|
|
"path/filepath"
|
2013-10-13 20:16:21 +08:00
|
|
|
"sync"
|
2013-10-06 15:01:39 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
downloadDir string
|
|
|
|
torrentPath string
|
|
|
|
mountDir string
|
|
|
|
)
|
|
|
|
|
2013-10-07 03:00:35 +08:00
|
|
|
const (
|
|
|
|
defaultMode = 0555
|
|
|
|
)
|
|
|
|
|
2013-10-06 15:01:39 +08:00
|
|
|
func init() {
|
|
|
|
flag.StringVar(&downloadDir, "downloadDir", "", "location to save torrent data")
|
|
|
|
flag.StringVar(&torrentPath, "torrentPath", func() string {
|
|
|
|
_user, err := user.Current()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
return filepath.Join(_user.HomeDir, ".config/transmission/torrents")
|
|
|
|
}(), "torrent files in this location describe the contents of the mounted filesystem")
|
|
|
|
flag.StringVar(&mountDir, "mountDir", "", "location the torrent contents are made available")
|
|
|
|
}
|
|
|
|
|
|
|
|
type TorrentFS struct {
|
2013-10-13 20:16:21 +08:00
|
|
|
Client *torrent.Client
|
|
|
|
DataSubs map[chan torrent.DataSpec]struct{}
|
|
|
|
sync.Mutex
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tfs *TorrentFS) publishData() {
|
|
|
|
for {
|
|
|
|
spec := <-tfs.Client.DataReady
|
|
|
|
tfs.Lock()
|
|
|
|
for ds := range tfs.DataSubs {
|
|
|
|
ds <- spec
|
|
|
|
}
|
|
|
|
tfs.Unlock()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tfs *TorrentFS) SubscribeData() chan torrent.DataSpec {
|
|
|
|
ch := make(chan torrent.DataSpec)
|
|
|
|
tfs.Lock()
|
|
|
|
tfs.DataSubs[ch] = struct{}{}
|
|
|
|
tfs.Unlock()
|
|
|
|
return ch
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tfs *TorrentFS) UnsubscribeData(ch chan torrent.DataSpec) {
|
|
|
|
go func() {
|
|
|
|
for _ = range ch {
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
tfs.Lock()
|
|
|
|
delete(tfs.DataSubs, ch)
|
|
|
|
tfs.Unlock()
|
|
|
|
close(ch)
|
2013-10-06 15:01:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
type rootNode struct {
|
|
|
|
fs *TorrentFS
|
|
|
|
}
|
|
|
|
|
2013-10-07 03:00:35 +08:00
|
|
|
type node struct {
|
|
|
|
path []string
|
|
|
|
metaInfo *metainfo.MetaInfo
|
2013-10-13 20:16:21 +08:00
|
|
|
FS *TorrentFS
|
|
|
|
InfoHash torrent.InfoHash
|
2013-10-07 03:00:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
type fileNode struct {
|
|
|
|
node
|
2013-10-13 20:16:21 +08:00
|
|
|
size uint64
|
|
|
|
TorrentOffset int64
|
2013-10-07 03:00:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
func (fn fileNode) Attr() (attr fuse.Attr) {
|
|
|
|
attr.Size = fn.size
|
|
|
|
attr.Mode = defaultMode
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-10-13 20:16:21 +08:00
|
|
|
func (fn fileNode) Read(req *fuse.ReadRequest, resp *fuse.ReadResponse, intr fusefs.Intr) fuse.Error {
|
|
|
|
if req.Dir {
|
|
|
|
panic("hodor")
|
|
|
|
}
|
|
|
|
dataSpecs := fn.FS.SubscribeData()
|
|
|
|
defer fn.FS.UnsubscribeData(dataSpecs)
|
|
|
|
data := make([]byte, func() int {
|
|
|
|
_len := int64(fn.size) - req.Offset
|
|
|
|
if int64(req.Size) < _len {
|
|
|
|
return req.Size
|
|
|
|
} else {
|
2013-10-14 22:39:12 +08:00
|
|
|
// limit read to the end of the file
|
2013-10-13 20:16:21 +08:00
|
|
|
return int(_len)
|
|
|
|
}
|
|
|
|
}())
|
2013-10-14 22:39:12 +08:00
|
|
|
infoHash := torrent.BytesInfoHash(fn.metaInfo.InfoHash)
|
|
|
|
torrentOff := fn.TorrentOffset + req.Offset
|
|
|
|
fn.FS.Client.PrioritizeDataRegion(infoHash, torrentOff, int64(len(data)))
|
2013-10-13 20:16:21 +08:00
|
|
|
for {
|
2013-10-14 22:39:12 +08:00
|
|
|
n, err := fn.FS.Client.TorrentReadAt(infoHash, torrentOff, data)
|
2013-10-15 16:42:30 +08:00
|
|
|
// log.Println(torrentOff, len(data), n, err)
|
2013-10-13 20:16:21 +08:00
|
|
|
switch err {
|
|
|
|
case nil:
|
|
|
|
resp.Data = data[:n]
|
|
|
|
return nil
|
|
|
|
case torrent.ErrDataNotReady:
|
|
|
|
select {
|
|
|
|
case <-dataSpecs:
|
|
|
|
case <-intr:
|
2013-10-14 22:39:12 +08:00
|
|
|
return fuse.EINTR
|
2013-10-13 20:16:21 +08:00
|
|
|
}
|
|
|
|
default:
|
|
|
|
log.Print(err)
|
|
|
|
return fuse.EIO
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-10-07 03:00:35 +08:00
|
|
|
type dirNode struct {
|
|
|
|
node
|
|
|
|
}
|
|
|
|
|
2013-10-13 20:16:21 +08:00
|
|
|
var (
|
|
|
|
_ fusefs.HandleReadDirer = dirNode{}
|
|
|
|
|
|
|
|
_ fusefs.HandleReader = fileNode{}
|
|
|
|
)
|
|
|
|
|
2013-10-07 03:00:35 +08:00
|
|
|
func isSubPath(parent, child []string) bool {
|
|
|
|
if len(child) <= len(parent) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for i := range parent {
|
|
|
|
if parent[i] != child[i] {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
func (dn dirNode) ReadDir(intr fusefs.Intr) (des []fuse.Dirent, err fuse.Error) {
|
|
|
|
names := map[string]bool{}
|
|
|
|
for _, fi := range dn.metaInfo.Files {
|
|
|
|
if !isSubPath(dn.path, fi.Path) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
name := fi.Path[len(dn.path)]
|
|
|
|
if names[name] {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
names[name] = true
|
|
|
|
de := fuse.Dirent{
|
|
|
|
Name: name,
|
|
|
|
}
|
|
|
|
if len(fi.Path) == len(dn.path)+1 {
|
|
|
|
de.Type = fuse.DT_File
|
|
|
|
} else {
|
|
|
|
de.Type = fuse.DT_Dir
|
|
|
|
}
|
|
|
|
des = append(des, de)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (dn dirNode) Lookup(name string, intr fusefs.Intr) (_node fusefs.Node, err fuse.Error) {
|
2013-10-13 20:16:21 +08:00
|
|
|
var torrentOffset int64
|
2013-10-07 03:00:35 +08:00
|
|
|
for _, fi := range dn.metaInfo.Files {
|
|
|
|
if !isSubPath(dn.path, fi.Path) {
|
2013-10-13 20:16:21 +08:00
|
|
|
torrentOffset += fi.Length
|
2013-10-07 03:00:35 +08:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
if fi.Path[len(dn.path)] != name {
|
2013-10-13 20:16:21 +08:00
|
|
|
torrentOffset += fi.Length
|
2013-10-07 03:00:35 +08:00
|
|
|
continue
|
|
|
|
}
|
2013-10-13 20:16:21 +08:00
|
|
|
__node := dn.node
|
|
|
|
__node.path = append(__node.path, name)
|
2013-10-07 03:00:35 +08:00
|
|
|
if len(fi.Path) == len(dn.path)+1 {
|
|
|
|
_node = fileNode{
|
2013-10-13 20:16:21 +08:00
|
|
|
node: __node,
|
|
|
|
size: uint64(fi.Length),
|
|
|
|
TorrentOffset: torrentOffset,
|
2013-10-07 03:00:35 +08:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
_node = dirNode{__node}
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if _node == nil {
|
|
|
|
err = fuse.ENOENT
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (dn dirNode) Attr() (attr fuse.Attr) {
|
|
|
|
attr.Mode = os.ModeDir | defaultMode
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func isSingleFileTorrent(mi *metainfo.MetaInfo) bool {
|
|
|
|
return len(mi.Files) == 1 && mi.Files[0].Path == nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (me rootNode) Lookup(name string, intr fusefs.Intr) (_node fusefs.Node, err fuse.Error) {
|
|
|
|
for _, _torrent := range me.fs.Client.Torrents() {
|
|
|
|
metaInfo := _torrent.MetaInfo
|
|
|
|
if metaInfo.Name == name {
|
|
|
|
__node := node{
|
|
|
|
metaInfo: metaInfo,
|
2013-10-13 20:16:21 +08:00
|
|
|
FS: me.fs,
|
|
|
|
InfoHash: torrent.BytesInfoHash(metaInfo.InfoHash),
|
2013-10-07 03:00:35 +08:00
|
|
|
}
|
|
|
|
if isSingleFileTorrent(metaInfo) {
|
2013-10-13 20:16:21 +08:00
|
|
|
_node = fileNode{__node, uint64(metaInfo.Files[0].Length), 0}
|
2013-10-07 03:00:35 +08:00
|
|
|
} else {
|
|
|
|
_node = dirNode{__node}
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if _node == nil {
|
|
|
|
err = fuse.ENOENT
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-10-06 15:01:39 +08:00
|
|
|
func (me rootNode) ReadDir(intr fusefs.Intr) (dirents []fuse.Dirent, err fuse.Error) {
|
|
|
|
for _, _torrent := range me.fs.Client.Torrents() {
|
|
|
|
metaInfo := _torrent.MetaInfo
|
|
|
|
dirents = append(dirents, fuse.Dirent{
|
|
|
|
Name: metaInfo.Name,
|
|
|
|
Type: func() fuse.DirentType {
|
2013-10-07 03:00:35 +08:00
|
|
|
if isSingleFileTorrent(metaInfo) {
|
2013-10-06 15:01:39 +08:00
|
|
|
return fuse.DT_File
|
|
|
|
} else {
|
|
|
|
return fuse.DT_Dir
|
|
|
|
}
|
|
|
|
}(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (rootNode) Attr() fuse.Attr {
|
|
|
|
return fuse.Attr{
|
|
|
|
Mode: os.ModeDir,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tfs *TorrentFS) Root() (fusefs.Node, fuse.Error) {
|
|
|
|
return rootNode{tfs}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
2013-10-13 20:16:21 +08:00
|
|
|
pprofAddr := flag.String("pprofAddr", "", "pprof HTTP server bind address")
|
2013-10-06 15:01:39 +08:00
|
|
|
flag.Parse()
|
2013-10-14 22:39:12 +08:00
|
|
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
2013-10-13 20:16:21 +08:00
|
|
|
if *pprofAddr != "" {
|
|
|
|
go http.ListenAndServe(*pprofAddr, nil)
|
|
|
|
}
|
|
|
|
defer profile.Start(profile.CPUProfile).Stop()
|
|
|
|
client := &torrent.Client{
|
|
|
|
DataDir: downloadDir,
|
|
|
|
DataReady: make(chan torrent.DataSpec),
|
|
|
|
}
|
|
|
|
client.Start()
|
2013-10-06 15:01:39 +08:00
|
|
|
torrentDir, err := os.Open(torrentPath)
|
|
|
|
defer torrentDir.Close()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
names, err := torrentDir.Readdirnames(-1)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
for _, name := range names {
|
|
|
|
metaInfo, err := metainfo.LoadFromFile(filepath.Join(torrentPath, name))
|
|
|
|
if err != nil {
|
|
|
|
log.Print(err)
|
|
|
|
}
|
2013-10-07 03:00:35 +08:00
|
|
|
err = client.AddTorrent(metaInfo)
|
|
|
|
if err != nil {
|
|
|
|
log.Print(err)
|
|
|
|
}
|
2013-10-14 22:39:12 +08:00
|
|
|
client.AddPeers(torrent.BytesInfoHash(metaInfo.InfoHash), []torrent.Peer{{
|
|
|
|
IP: net.IPv4(127, 0, 0, 1),
|
|
|
|
Port: 3000,
|
|
|
|
}})
|
2013-10-06 15:01:39 +08:00
|
|
|
}
|
|
|
|
conn, err := fuse.Mount(mountDir)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2013-10-13 20:16:21 +08:00
|
|
|
fs := &TorrentFS{
|
|
|
|
Client: client,
|
|
|
|
DataSubs: make(map[chan torrent.DataSpec]struct{}),
|
|
|
|
}
|
|
|
|
go fs.publishData()
|
2013-10-06 15:01:39 +08:00
|
|
|
fusefs.Serve(conn, fs)
|
|
|
|
}
|