package torrent import ( "context" "crypto/sha1" "encoding/hex" "fmt" "os" "sync" "time" ) type StubClient struct { logPath string savePath string mu sync.RWMutex logMu sync.Mutex torrents map[string]*TorrentInfo } func NewStubClient(logPath, savePath string) *StubClient { return &StubClient{ logPath: logPath, savePath: savePath, torrents: make(map[string]*TorrentInfo), } } func (c *StubClient) log(format string, args ...any) { if c.logPath == "" { return } c.logMu.Lock() defer c.logMu.Unlock() f, err := os.OpenFile(c.logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return } defer f.Close() timestamp := time.Now().Format(time.RFC3339) msg := fmt.Sprintf(format, args...) fmt.Fprintf(f, "[%s] %s\n", timestamp, msg) } func (c *StubClient) Connect(ctx context.Context) error { c.log("CONNECT") return nil } func (c *StubClient) Disconnect(ctx context.Context) error { c.log("DISCONNECT") return nil } func (c *StubClient) ListTorrents(ctx context.Context) ([]TorrentInfo, error) { c.mu.RLock() defer c.mu.RUnlock() c.log("LIST_TORRENTS count=%d", len(c.torrents)) result := make([]TorrentInfo, 0, len(c.torrents)) for _, t := range c.torrents { result = append(result, *t) } return result, nil } func (c *StubClient) GetTorrent(ctx context.Context, hash string) (*TorrentInfo, error) { c.mu.RLock() defer c.mu.RUnlock() c.log("GET_TORRENT hash=%s", hash) t, ok := c.torrents[hash] if !ok { return nil, ErrTorrentNotFound } return t, nil } func (c *StubClient) AddTorrentURL(ctx context.Context, url string, savePath *string) error { path := c.savePath if savePath != nil { path = *savePath } hash := generateHashFromURL(url) name := "Torrent-" + hash[:8] c.mu.Lock() c.torrents[hash] = &TorrentInfo{ Hash: hash, Name: name, Size: 500 * 1024 * 1024, Progress: 0, DownloadSpeed: 0, UploadSpeed: 0, State: StateQueued, SavePath: path, } c.mu.Unlock() c.log("ADD_TORRENT_URL url=%s hash=%s save_path=%s", url, hash, path) return nil } func (c *StubClient) AddTorrentFile(ctx context.Context, data []byte, savePath *string) error { path := c.savePath if savePath != nil { path = *savePath } hash := generateHashFromData(data) name := "Torrent-" + hash[:8] c.mu.Lock() c.torrents[hash] = &TorrentInfo{ Hash: hash, Name: name, Size: uint64(len(data) * 100), Progress: 0, DownloadSpeed: 0, UploadSpeed: 0, State: StateQueued, SavePath: path, } c.mu.Unlock() c.log("ADD_TORRENT_FILE size=%d hash=%s save_path=%s", len(data), hash, path) return nil } func (c *StubClient) RemoveTorrent(ctx context.Context, hash string, deleteFiles bool) error { c.mu.Lock() delete(c.torrents, hash) c.mu.Unlock() c.log("REMOVE_TORRENT hash=%s delete_files=%t", hash, deleteFiles) return nil } func (c *StubClient) PauseTorrent(ctx context.Context, hash string) error { c.mu.Lock() if t, ok := c.torrents[hash]; ok { t.State = StatePaused t.DownloadSpeed = 0 } c.mu.Unlock() c.log("PAUSE_TORRENT hash=%s", hash) return nil } func (c *StubClient) ResumeTorrent(ctx context.Context, hash string) error { c.mu.Lock() if t, ok := c.torrents[hash]; ok { if t.Progress < 1.0 { t.State = StateDownloading } else { t.State = StateSeeding } } c.mu.Unlock() c.log("RESUME_TORRENT hash=%s", hash) return nil } func (c *StubClient) SetTorrentState(hash string, state TorrentState, progress float64) { c.mu.Lock() defer c.mu.Unlock() if t, ok := c.torrents[hash]; ok { t.State = state t.Progress = progress if state == StateSeeding { t.Progress = 1.0 } } } func (c *StubClient) SetTorrentName(hash, name string) { c.mu.Lock() defer c.mu.Unlock() if t, ok := c.torrents[hash]; ok { t.Name = name } } func (c *StubClient) AddTorrentDirect(info TorrentInfo) { c.mu.Lock() defer c.mu.Unlock() c.torrents[info.Hash] = &info } func (c *StubClient) Clear() { c.mu.Lock() defer c.mu.Unlock() c.torrents = make(map[string]*TorrentInfo) } func (c *StubClient) GetAllTorrents() map[string]*TorrentInfo { c.mu.RLock() defer c.mu.RUnlock() result := make(map[string]*TorrentInfo, len(c.torrents)) for k, v := range c.torrents { copy := *v result[k] = © } return result } func generateHashFromURL(url string) string { h := sha1.New() h.Write([]byte(url)) return hex.EncodeToString(h.Sum(nil)) } func generateHashFromData(data []byte) string { h := sha1.New() h.Write(data) return hex.EncodeToString(h.Sum(nil)) }