|
2 | 2 | [](https://godoc.org/github.com/KarpelesLab/smartremote) |
3 | 3 | [](https://coveralls.io/github/KarpelesLab/smartremote?branch=master) |
4 | 4 |
|
5 | | - |
6 | 5 | # SmartRemote |
7 | 6 |
|
8 | | -NOTE: this is not a remote for your TV, just an easy way to access remote (http) files. |
| 7 | +SmartRemote is a Go library that provides seamless access to remote HTTP files with intelligent partial downloading and local caching. Rather than downloading entire files upfront, it allows you to open a URL and read from it like a regular file, automatically fetching only the needed portions on-demand. |
| 8 | + |
| 9 | +This is particularly useful for large files like ISOs, ZIPs, and other archives where you might only need to access specific sections (e.g., reading the central directory of a ZIP file without downloading the entire archive). |
| 10 | + |
| 11 | +## Features |
| 12 | + |
| 13 | +- **Lazy Loading**: Downloads only the blocks you actually read |
| 14 | +- **Resume Support**: Partial downloads can be saved and resumed via `.part` files |
| 15 | +- **Intelligent Seeking**: Handles seekable HTTP connections using Range requests |
| 16 | +- **Concurrent Downloads**: Manages multiple concurrent download clients with configurable limits |
| 17 | +- **Idle Background Downloading**: Automatically fills gaps in partial downloads when not actively reading |
| 18 | +- **Block-Based Tracking**: Uses efficient RoaringBitmap for tracking downloaded 64KB blocks |
| 19 | +- **Standard Interfaces**: Implements `io.Reader`, `io.ReaderAt`, and `io.Seeker` |
| 20 | + |
| 21 | +## Installation |
| 22 | + |
| 23 | +```bash |
| 24 | +go get github.com/KarpelesLab/smartremote |
| 25 | +``` |
| 26 | + |
| 27 | +## Quick Start |
| 28 | + |
| 29 | +```go |
| 30 | +package main |
| 31 | + |
| 32 | +import ( |
| 33 | + "fmt" |
| 34 | + "io" |
| 35 | + "github.com/KarpelesLab/smartremote" |
| 36 | +) |
| 37 | + |
| 38 | +func main() { |
| 39 | + // Open a remote file |
| 40 | + f, err := smartremote.Open("https://example.com/largefile.zip") |
| 41 | + if err != nil { |
| 42 | + panic(err) |
| 43 | + } |
| 44 | + defer f.Close() |
| 45 | + |
| 46 | + // Use f as a regular read-only file |
| 47 | + // It will download parts as needed from the remote URL |
| 48 | + buf := make([]byte, 1024) |
| 49 | + n, err := f.Read(buf) |
| 50 | + if err != nil && err != io.EOF { |
| 51 | + panic(err) |
| 52 | + } |
| 53 | + fmt.Printf("Read %d bytes\n", n) |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +## How It Works |
| 58 | + |
| 59 | +### Block-Based Downloads |
9 | 60 |
|
10 | | -How to use: |
| 61 | +SmartRemote divides remote files into 64KB blocks. When you read from the file, only the blocks containing the requested data are downloaded. Downloaded blocks are: |
11 | 62 |
|
12 | | -```Go |
13 | | - f, err := smartremote.Open("http://...") |
14 | | - if err != nil { |
15 | | - panic(err) |
16 | | - } |
| 63 | +1. Stored in a local temporary file |
| 64 | +2. Tracked using a RoaringBitmap for efficient status checking |
| 65 | +3. Persisted to a `.part` file so downloads can be resumed |
17 | 66 |
|
18 | | - // Use "f" as a regular readonly file, it'll download parts as needed from the remote url |
| 67 | +### HTTP Range Requests |
| 68 | + |
| 69 | +The library uses HTTP Range requests (status 206 Partial Content) to download specific byte ranges. If the server doesn't support Range requests, SmartRemote falls back to downloading the entire file. |
| 70 | + |
| 71 | +### Connection Pooling |
| 72 | + |
| 73 | +The `DownloadManager` maintains a pool of HTTP connections (default: 10) that are reused across requests. Idle connections are automatically cleaned up after 5 minutes. |
| 74 | + |
| 75 | +### Background Downloading |
| 76 | + |
| 77 | +When there are no active read requests, SmartRemote opportunistically downloads missing blocks in the background, progressively completing the file. |
| 78 | + |
| 79 | +## Advanced Usage |
| 80 | + |
| 81 | +### Custom DownloadManager |
| 82 | + |
| 83 | +Create a custom `DownloadManager` for more control: |
| 84 | + |
| 85 | +```go |
| 86 | +dm := smartremote.NewDownloadManager() |
| 87 | +dm.MaxConcurrent = 5 // Limit to 5 concurrent connections |
| 88 | +dm.MaxDataJump = 1024 * 1024 // Allow skipping up to 1MB when seeking |
| 89 | +dm.TmpDir = "/custom/tmp" // Custom temp directory |
| 90 | +dm.Client = customHTTPClient // Use a custom http.Client |
| 91 | + |
| 92 | +f, err := dm.Open("https://example.com/file.iso") |
| 93 | +if err != nil { |
| 94 | + panic(err) |
| 95 | +} |
| 96 | +defer f.Close() |
19 | 97 | ``` |
20 | 98 |
|
21 | | -This can be used with any kind of file as long as the server supports resume. |
22 | | -If it doesn't then this will just download the whole file, and still work the |
23 | | -same. |
| 99 | +### Specify Local Storage Path |
| 100 | + |
| 101 | +Store the downloaded file at a specific path: |
| 102 | + |
| 103 | +```go |
| 104 | +dm := smartremote.NewDownloadManager() |
| 105 | +f, err := dm.OpenTo("https://example.com/file.iso", "/path/to/local/file.iso") |
| 106 | +if err != nil { |
| 107 | + panic(err) |
| 108 | +} |
| 109 | +defer f.Close() |
| 110 | +``` |
| 111 | + |
| 112 | +### Simple ReaderAt Interface |
| 113 | + |
| 114 | +For simple use cases where you just need `io.ReaderAt`: |
| 115 | + |
| 116 | +```go |
| 117 | +dm := smartremote.NewDownloadManager() |
| 118 | +reader := dm.For("https://example.com/file.bin") |
| 119 | + |
| 120 | +buf := make([]byte, 100) |
| 121 | +n, err := reader.ReadAt(buf, 1000) // Read 100 bytes starting at offset 1000 |
| 122 | +``` |
| 123 | + |
| 124 | +### Force Complete Download |
| 125 | + |
| 126 | +Download the entire file: |
| 127 | + |
| 128 | +```go |
| 129 | +f, err := smartremote.Open("https://example.com/file.zip") |
| 130 | +if err != nil { |
| 131 | + panic(err) |
| 132 | +} |
| 133 | +defer f.Close() |
| 134 | + |
| 135 | +// Download everything |
| 136 | +err = f.Complete() |
| 137 | +if err != nil { |
| 138 | + panic(err) |
| 139 | +} |
| 140 | +``` |
| 141 | + |
| 142 | +### Manual Progress Saving |
| 143 | + |
| 144 | +Manually trigger a save of download progress: |
| 145 | + |
| 146 | +```go |
| 147 | +f, err := smartremote.Open("https://example.com/file.zip") |
| 148 | +if err != nil { |
| 149 | + panic(err) |
| 150 | +} |
| 151 | + |
| 152 | +// ... perform some reads ... |
| 153 | + |
| 154 | +// Save progress explicitly |
| 155 | +err = f.SavePart() |
| 156 | +if err != nil { |
| 157 | + panic(err) |
| 158 | +} |
| 159 | +``` |
| 160 | + |
| 161 | +## Configuration Options |
| 162 | + |
| 163 | +| Option | Type | Default | Description | |
| 164 | +|--------|------|---------|-------------| |
| 165 | +| `MaxConcurrent` | `int` | 10 | Maximum number of concurrent HTTP connections | |
| 166 | +| `MaxDataJump` | `int64` | 512KB | Maximum bytes to read and discard when seeking forward (vs opening a new connection) | |
| 167 | +| `TmpDir` | `string` | `os.TempDir()` | Directory for temporary download files | |
| 168 | +| `Client` | `*http.Client` | `http.DefaultClient` | HTTP client for making requests | |
| 169 | +| `Logger` | `*log.Logger` | stderr | Logger for debug output | |
| 170 | + |
| 171 | +## API Reference |
| 172 | + |
| 173 | +### Package Functions |
| 174 | + |
| 175 | +- `Open(url string) (*File, error)` - Open a remote URL using the default manager |
| 176 | + |
| 177 | +### DownloadManager |
| 178 | + |
| 179 | +- `NewDownloadManager() *DownloadManager` - Create a new download manager |
| 180 | +- `Open(url string) (*File, error)` - Open a URL with auto-generated local path |
| 181 | +- `OpenTo(url, localPath string) (*File, error)` - Open a URL with specific local path |
| 182 | +- `For(url string) io.ReaderAt` - Get a simple ReaderAt for a URL |
| 183 | + |
| 184 | +### File |
| 185 | + |
| 186 | +- `Read(p []byte) (n int, err error)` - Read from current position |
| 187 | +- `ReadAt(p []byte, off int64) (int, error)` - Read from specific offset |
| 188 | +- `Seek(offset int64, whence int) (int64, error)` - Seek to position |
| 189 | +- `Close() error` - Close file and save progress |
| 190 | +- `GetSize() (int64, error)` - Get remote file size |
| 191 | +- `SetSize(size int64)` - Manually set file size |
| 192 | +- `Stat() (os.FileInfo, error)` - Get file info |
| 193 | +- `Complete() error` - Download entire file |
| 194 | +- `SavePart() error` - Manually save download progress |
| 195 | + |
| 196 | +## Resume Behavior |
| 197 | + |
| 198 | +When opening a URL: |
| 199 | + |
| 200 | +1. If the local file doesn't exist, a new download begins |
| 201 | +2. If the local file exists with a `.part` file, the download resumes from where it left off |
| 202 | +3. If the local file exists without a `.part` file, it's assumed to be complete |
| 203 | + |
| 204 | +On close: |
| 205 | +- If download is incomplete and progress was saved successfully, both files are kept for resume |
| 206 | +- If download is incomplete and progress save failed, the partial file is deleted |
| 207 | +- If download is complete, the `.part` file is removed |
| 208 | + |
| 209 | +## Requirements |
| 210 | + |
| 211 | +- Go 1.18 or later |
| 212 | +- Server must support HTTP Range requests for partial downloads (falls back to full download otherwise) |
24 | 213 |
|
25 | | -# TODO |
| 214 | +## TODO |
26 | 215 |
|
27 | | -* Add support for range invalidation (bad checksum causes re-download of affected area) |
28 | | -* Refactor idle downloader for better performances |
| 216 | +- Add support for range invalidation (bad checksum causes re-download of affected area) |
| 217 | +- Refactor idle downloader for better performance |
0 commit comments