Peers join a room and the relay just broadcasts membership (id + dialable address); peers then connect to each other directly, PAKE-handshake per link using the room codephrase, and exchange 1MB chunks. Resume reuses croc's zero-block scan to detect already-received chunks, whole-file hash verifies completion, and every peer keeps reseeding what it has to others until stopped (--one-time restores exit-on-complete for a quick fetch).
161 lines
3.8 KiB
Go
161 lines
3.8 KiB
Go
package swarm
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"os"
|
|
"sync"
|
|
|
|
"github.com/tzgyn/share/src/utils"
|
|
)
|
|
|
|
// ChunkSize is fixed at 1MB: large enough to keep protocol overhead low,
|
|
// small enough that resuming a transfer never re-fetches much.
|
|
const ChunkSize = 1 << 20
|
|
|
|
// NumChunks returns how many ChunkSize pieces a file of size bytes has.
|
|
func NumChunks(size int64) int64 {
|
|
if size <= 0 {
|
|
return 0
|
|
}
|
|
return (size + ChunkSize - 1) / ChunkSize
|
|
}
|
|
|
|
func chunkLen(size, offset int64) int64 {
|
|
if offset+ChunkSize > size {
|
|
return size - offset
|
|
}
|
|
return ChunkSize
|
|
}
|
|
|
|
// ChunkStore tracks on-disk chunk presence for one file in the swarm and
|
|
// reads/writes it at chunk granularity.
|
|
type ChunkStore struct {
|
|
mu sync.Mutex
|
|
path string
|
|
size int64
|
|
have map[int64]bool // chunk offset -> present
|
|
}
|
|
|
|
// OpenChunkStore opens (or creates) path as a file of exactly size bytes
|
|
// for receiving into, and figures out which chunks are already there. This
|
|
// reuses the same "a chunk-sized block that's still all zero bytes hasn't
|
|
// arrived yet" scan croc itself uses, so resuming an interrupted transfer
|
|
// needs no separate manifest of what's already done.
|
|
func OpenChunkStore(path string, size int64) (*ChunkStore, error) {
|
|
info, statErr := os.Stat(path)
|
|
fresh := statErr != nil || info.Size() != size
|
|
|
|
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o644)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if fresh {
|
|
err = f.Truncate(size)
|
|
}
|
|
f.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cs := &ChunkStore{path: path, size: size, have: make(map[int64]bool)}
|
|
if !fresh {
|
|
if err := cs.scanExisting(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return cs, nil
|
|
}
|
|
|
|
// NewSeedChunkStore wraps an already-complete local file being shared into
|
|
// the swarm: no scanning, every chunk is simply present. The file is never
|
|
// truncated or modified.
|
|
func NewSeedChunkStore(path string, size int64) *ChunkStore {
|
|
cs := &ChunkStore{path: path, size: size, have: make(map[int64]bool)}
|
|
for offset := int64(0); offset < size; offset += ChunkSize {
|
|
cs.have[offset] = true
|
|
}
|
|
return cs
|
|
}
|
|
|
|
func (cs *ChunkStore) scanExisting() error {
|
|
f, err := os.Open(cs.path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
zero := make([]byte, ChunkSize)
|
|
buf := make([]byte, ChunkSize)
|
|
for offset := int64(0); offset < cs.size; offset += ChunkSize {
|
|
l := chunkLen(cs.size, offset)
|
|
if _, err := f.ReadAt(buf[:l], offset); err != nil && err != io.EOF {
|
|
return err
|
|
}
|
|
if !bytes.Equal(buf[:l], zero[:l]) {
|
|
cs.have[offset] = true
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (cs *ChunkStore) HaveOffsets() []int64 {
|
|
cs.mu.Lock()
|
|
defer cs.mu.Unlock()
|
|
list := make([]int64, 0, len(cs.have))
|
|
for offset := range cs.have {
|
|
list = append(list, offset)
|
|
}
|
|
return list
|
|
}
|
|
|
|
func (cs *ChunkStore) HasChunk(offset int64) bool {
|
|
cs.mu.Lock()
|
|
defer cs.mu.Unlock()
|
|
return cs.have[offset]
|
|
}
|
|
|
|
func (cs *ChunkStore) Complete() bool {
|
|
cs.mu.Lock()
|
|
defer cs.mu.Unlock()
|
|
return int64(len(cs.have)) == NumChunks(cs.size)
|
|
}
|
|
|
|
func (cs *ChunkStore) ReadChunk(offset int64) ([]byte, error) {
|
|
f, err := os.Open(cs.path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
buf := make([]byte, chunkLen(cs.size, offset))
|
|
if _, err := f.ReadAt(buf, offset); err != nil && err != io.EOF {
|
|
return nil, err
|
|
}
|
|
return buf, nil
|
|
}
|
|
|
|
func (cs *ChunkStore) WriteChunk(offset int64, data []byte) error {
|
|
f, err := os.OpenFile(cs.path, os.O_WRONLY, 0o644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
if _, err := f.WriteAt(data, offset); err != nil {
|
|
return err
|
|
}
|
|
cs.mu.Lock()
|
|
cs.have[offset] = true
|
|
cs.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// VerifyHash confirms a completed file matches the expected whole-file
|
|
// hash from the manifest.
|
|
func (cs *ChunkStore) VerifyHash(expected []byte) (bool, error) {
|
|
got, err := utils.HashFile(cs.path, "xxhash")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return bytes.Equal(got, expected), nil
|
|
}
|