Skip to content

Commit faf78ea

Browse files
committed
Refactor: simplify options with reflection and rename project to rclone-vfs
- Implemented a tag-driven reflection system for vfsproxy.Options to automate flag registration, Caddyfile parsing, and rclone configuration. - Renamed project from 'vfscache-proxy' to 'rclone-vfs' across module, imports, Dockerfile, and documentation. - Added comprehensive tests for reflection-based configuration in both pkg/vfsproxy and caddy modules. - Updated README.md to reflect the new options system and remove obsolete metadata cache references.
1 parent eb96744 commit faf78ea

File tree

9 files changed

+217
-111
lines changed

9 files changed

+217
-111
lines changed

.goreleaser.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
version: 2
2-
project_name: vfscache_proxy
2+
project_name: rclone-vfs
33

44
builds:
55
- env:
66
- CGO_ENABLED=0
77
main: main.go
8+
binary: rclone-vfs
89
flags: -trimpath
910
ldflags:
1011
- -s -w

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ RUN apk add --no-cache ca-certificates tzdata
44

55
ARG TARGETPLATFORM
66

7-
COPY $TARGETPLATFORM/vfscache_proxy /vfscache_proxy
7+
COPY $TARGETPLATFORM/rclone-vfs /rclone-vfs
88

9-
CMD ["/vfscache_proxy"]
9+
CMD ["/rclone-vfs"]

README.md

Lines changed: 33 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,154 +1,92 @@
1-
# VFS Cache Proxy
1+
# rclone-vfs
22

33
A high-performance streaming proxy server built on top of [Rclone's VFS](https://rclone.org/commands/rclone_mount/#vfs-file-caching) layer. This service allows you to stream files from any HTTP/HTTPS URL with the benefits of Rclone's smart caching, buffering, and read-ahead capabilities.
44

5-
It also features an internal metadata cache using `freecache` to minimize upstream requests for file size and modification times.
6-
75
## Features
86

97
- **Rclone VFS Integration**: Leverages Rclone's robust VFS for disk caching, sparse file support, and efficient streaming.
10-
- **Smart Metadata Caching**: In-memory caching of file size and modification times to reduce latency and upstream API calls.
118
- **Flexible Input**: Supports passing target URLs via query parameters or Base64-encoded paths.
12-
- **Deduplication**: Optional query parameter stripping to cache identical files with different access tokens together.
13-
- **Docker Ready**: minimal Alpine-based Docker image.
9+
- **Deduplication**: Optional query parameter and domain stripping to maximize cache hits for mirrored content.
10+
- **Caddy Ready**: Includes a native Caddy module for easy integration into your web server.
11+
- **Docker Ready**: Minimal Alpine-based Docker image.
1412

1513
## Getting Started
1614

1715
### Prerequisites
1816

19-
- Docker
17+
- Docker or Go 1.25+
2018

2119
### Installation & Run
2220

23-
You can run the VFS Proxy directly using Docker. This will pull the image (if hosted) or you can build it locally.
24-
25-
To run the server with a persistent cache directory:
21+
You can run `rclone-vfs` directly using Docker:
2622

2723
```bash
2824
docker run -d \
2925
-p 8080:8080 \
30-
-v /path/to/host/cache:/app/cache \
31-
vfs-proxy --cache-dir /app/cache
26+
-v /path/to/host/cache:/tmp/rclone_vfs_cache \
27+
ghcr.io/tgdrive/rclone-vfs --cache-dir /tmp/rclone_vfs_cache
3228
```
3329

34-
You can pass any CLI flags (see below) to the end of the docker run command.
35-
3630
## CLI Flags
3731

32+
All Rclone VFS settings are supported. Below are the most common ones:
33+
3834
| Flag | Default | Description |
3935
|------|---------|-------------|
4036
| `--port` | `8080` | Port to listen on. |
4137
| `--cache-dir` | System Temp | Directory to store the VFS disk cache. |
38+
| `--cache-mode` | `off` | VFS cache mode (`off`, `minimal`, `writes`, `full`). |
4239
| `--chunk-size` | `64M` | The chunk size for read requests. |
4340
| `--chunk-streams` | `2` | Number of parallel streams to read at once. |
4441
| `--max-age` | `1h` | Max age of files in the VFS cache. |
4542
| `--max-size` | `off` | Max total size of objects in the cache. |
46-
| `--strip-query` | `false` | If true, strips query parameters from the URL when generating the cache key. Useful for signed URLs. |
47-
| `--strip-domain` | `false` | If true, strips domain and protocol from the URL when generating the cache key. Useful for content mirrored across multiple domains. |
48-
| `--metadata-cache-size` | `5M` | Size of the in-memory metadata cache. |
43+
| `--strip-query` | `false` | If true, strips query parameters from the URL when generating the cache key. |
44+
| `--strip-domain` | `false` | If true, strips domain and protocol from the URL. |
45+
| `--shard-level` | `1` | Number of directory levels for sharding the cache. |
46+
47+
*Run `rclone-vfs --help` to see all available flags, including advanced VFS permissions and timing settings.*
4948

5049
## API Endpoints
5150

5251
### 1. Stream via Query Parameter
53-
5452
```http
5553
GET /stream?url=https://example.com/video.mp4
5654
```
5755

5856
### 2. Stream via Base64 Path
59-
60-
Useful for tools or players that struggle with query parameters in URLs.
61-
62-
1. Base64 encode your target URL (URL-safe or standard).
63-
- `https://example.com/video.mp4` -> `aHR0cHM6Ly9leGFtcGxlLmNvbS92aWRlby5tcDQ`
64-
2. Request the path:
65-
66-
```http
67-
GET /stream/aHR0cHM6Ly9leGFtcGxlLmNvbS92aWRlby5tcDQ
68-
```
57+
Useful for players that struggle with query parameters.
58+
1. Base64 encode your URL: `https://example.com/video.mp4` -> `aHR0cHM6Ly9leGFtcGxlLmNvbS92aWRlby5tcDQ`
59+
2. Request: `GET /stream/aHR0cHM6Ly9leGFtcGxlLmNvbS92aWRlby5tcDQ`
6960

7061
## Caddy Plugin
7162

72-
VFS Cache Proxy can be used as a Caddy module. This allows Caddy to act as a high-performance caching reverse proxy for specific upstreams.
73-
74-
### 1. Build with xcaddy
75-
76-
Since this is a Go plugin, you must compile it into Caddy:
77-
63+
Build Caddy with the module:
7864
```bash
79-
xcaddy build \
80-
--with github.com/tgdrive/vfscache-proxy/caddy
65+
xcaddy build --with github.com/tgdrive/rclone-vfs/caddy
8166
```
8267

83-
### 2. Caddyfile Usage
84-
85-
#### Pattern A: Simple Reverse Proxy
86-
Acts as a transparent caching layer for an upstream server.
87-
68+
### Caddyfile Usage
8869
```caddyfile
8970
:8080 {
9071
vfs https://upstream.com {
9172
cache_dir /var/cache/vfs
73+
cache_mode full
9274
max_age 24h
93-
}
94-
}
95-
```
96-
*Request:* `GET /movie.mp4` -> *Fetches & Caches:* `https://upstream.com/movie.mp4`
97-
98-
#### Pattern B: Specific Route
99-
Only proxy specific paths through the VFS layer.
100-
101-
```caddyfile
102-
example.com {
103-
route /media/* {
104-
vfs https://s3.amazonaws.com/my-bucket {
105-
chunk_size 128M
106-
chunk_streams 4
107-
}
108-
}
109-
110-
# Other standard Caddy logic
111-
file_server browse
112-
}
113-
```
114-
115-
#### Pattern C: Stripping Prefix (`handle_path`)
116-
Useful if the upstream does not expect the local path prefix.
117-
118-
```caddyfile
119-
:8080 {
120-
handle_path /stream/* {
121-
vfs https://cdn.example.net {
122-
strip_domain
123-
strip_query
124-
}
75+
strip_query
12576
}
12677
}
12778
```
12879

12980
### Caddyfile Directives
130-
131-
| Directive | Description |
132-
|-----------|-------------|
133-
| `upstream` (arg) | **Required**. The base URL of the source server. |
134-
| `cache_dir` | Directory for the VFS disk cache. |
135-
| `max_age` | Max age of files in cache (e.g., `1h`, `24h`). |
136-
| `max_size` | Max total size of the disk cache. |
137-
| `chunk_size` | Chunk size for read requests (default `64M`). |
138-
| `chunk_streams` | Parallel download streams per request. |
139-
| `strip_query` | Strip query parameters for the metadata cache key. |
140-
| `strip_domain` | Strip domain/protocol for the metadata cache key. |
141-
| `metadata_cache_size` | Size of the in-memory metadata cache (default `5M`). |
142-
| `fs_name` | Custom name for the VFS file system. |
143-
| `cache_mode` | VFS cache mode (`off`, `minimal`, `writes`, `full`). |
144-
145-
---
81+
- `upstream` (argument): The base URL of the source server.
82+
- `cache_dir`: Path to disk cache.
83+
- `cache_mode`: `off`, `minimal`, `writes`, or `full`.
84+
- `max_age`, `max_size`, `chunk_size`, `chunk_streams`.
85+
- `strip_query`, `strip_domain`, `shard-level`.
86+
- `read_only`, `no_seek`, `no_checksum`, etc.
14687

14788
## How it Works
14889

149-
1. **Request**: The server receives a request for a URL.
150-
2. **Metadata**: It checks an in-memory `freecache` for the file's size and modification time.
151-
- If missing, it performs an HTTP `HEAD` (or `GET` range) request to the upstream URL and caches the result for 1 hour.
152-
3. **VFS Mount**: The file is virtually "mounted" using the `link` backend.
153-
4. **Streaming**: Rclone's VFS layer handles the reading, downloading chunks in parallel (`--chunk-streams`), and caching them to disk (`--cache-dir`).
154-
5. **Response**: The content is streamed to the client with support for Range requests (seeking).
90+
1. **VFS Mapping**: The requested URL is mapped to a unique deterministic path in a virtual rclone file system.
91+
2. **Streaming**: Rclone's VFS layer handles the heavy lifting—on-demand downloading, parallel chunk streaming, and local disk persistence.
92+
3. **Efficiency**: Range requests are fully supported, allowing clients to seek through large files without downloading the entire file.

caddy/vfs.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
1616
"go.uber.org/zap"
1717

18-
"github.com/tgdrive/vfscache-proxy/pkg/vfsproxy"
18+
"github.com/tgdrive/rclone-vfs/pkg/vfsproxy"
1919
)
2020

2121
func init() {

caddy/vfs_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package vfs
2+
3+
import (
4+
"testing"
5+
6+
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
7+
"github.com/tgdrive/rclone-vfs/pkg/vfsproxy"
8+
)
9+
10+
func TestUnmarshalCaddyfile(t *testing.T) {
11+
d := caddyfile.NewTestDispenser(`
12+
vfs https://example.com {
13+
cache_dir /tmp/cache
14+
cache_mode full
15+
max_age 24h
16+
chunk_streams 4
17+
strip_query
18+
shard-level 3
19+
read_only
20+
}
21+
`)
22+
23+
v := &VFS{
24+
Options: vfsproxy.DefaultOptions(),
25+
}
26+
27+
err := v.UnmarshalCaddyfile(d)
28+
if err != nil {
29+
t.Fatalf("failed to unmarshal caddyfile: %v", err)
30+
}
31+
32+
if v.Upstream != "https://example.com" {
33+
t.Errorf("expected Upstream 'https://example.com', got '%s'", v.Upstream)
34+
}
35+
36+
// Test reflection-mapped string options
37+
if v.CacheDir != "/tmp/cache" {
38+
t.Errorf("expected CacheDir '/tmp/cache', got '%s'", v.CacheDir)
39+
}
40+
if v.CacheMode != "full" {
41+
t.Errorf("expected CacheMode 'full', got '%s'", v.CacheMode)
42+
}
43+
if v.CacheMaxAge != "24h" {
44+
t.Errorf("expected CacheMaxAge '24h', got '%s'", v.CacheMaxAge)
45+
}
46+
47+
// Test reflection-mapped integer options
48+
if v.CacheChunkStreams != 4 {
49+
t.Errorf("expected CacheChunkStreams 4, got %d", v.CacheChunkStreams)
50+
}
51+
if v.ShardLevel != 3 {
52+
t.Errorf("expected ShardLevel 3, got %d", v.ShardLevel)
53+
}
54+
55+
// Test reflection-mapped boolean flags
56+
if !v.StripQuery {
57+
t.Error("expected StripQuery to be true")
58+
}
59+
if !v.ReadOnly {
60+
t.Error("expected ReadOnly to be true")
61+
}
62+
63+
// Test defaults for things not in the Caddyfile
64+
if v.FsName != "rclone-vfs" {
65+
t.Errorf("expected default FsName 'rclone-vfs', got '%s'", v.FsName)
66+
}
67+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module github.com/tgdrive/vfscache-proxy
1+
module github.com/tgdrive/rclone-vfs
22

33
go 1.25.6
44

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"syscall"
1212
"time"
1313

14-
"github.com/tgdrive/vfscache-proxy/pkg/vfsproxy"
14+
"github.com/tgdrive/rclone-vfs/pkg/vfsproxy"
1515

1616
"github.com/rclone/rclone/fs/config"
1717
"github.com/spf13/pflag"

pkg/vfsproxy/options_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package vfsproxy
2+
3+
import (
4+
"testing"
5+
6+
"github.com/spf13/pflag"
7+
)
8+
9+
func TestDefaultOptions(t *testing.T) {
10+
opt := DefaultOptions()
11+
12+
// Test default values from tags
13+
if opt.FsName != "rclone-vfs" {
14+
t.Errorf("expected FsName 'rclone-vfs', got '%s'", opt.FsName)
15+
}
16+
if opt.ShardLevel != 1 {
17+
t.Errorf("expected ShardLevel 1, got %d", opt.ShardLevel)
18+
}
19+
20+
// Test a value that should come from rclone defaults (vfscommon.Opt)
21+
// rclone default for vfs_read_chunk_size is usually "128Mi" or similar
22+
if opt.CacheChunkSize == "" {
23+
t.Error("expected CacheChunkSize to be populated from rclone defaults, but it's empty")
24+
}
25+
}
26+
27+
func TestToConfigMap(t *testing.T) {
28+
opt := Options{
29+
CacheMode: "full",
30+
CacheMaxAge: "24h",
31+
ReadOnly: true,
32+
ShardLevel: 5, // Has no vfs tag, should be ignored
33+
StripQuery: true, // Has no vfs tag, should be ignored
34+
}
35+
36+
m := opt.ToConfigMap()
37+
38+
if m["vfs_cache_mode"] != "full" {
39+
t.Errorf("expected vfs_cache_mode 'full', got '%s'", m["vfs_cache_mode"])
40+
}
41+
if m["vfs_cache_max_age"] != "24h" {
42+
t.Errorf("expected vfs_cache_max_age '24h', got '%s'", m["vfs_cache_max_age"])
43+
}
44+
if m["read_only"] != "true" {
45+
t.Errorf("expected read_only 'true', got '%s'", m["read_only"])
46+
}
47+
48+
// Check that fields with vfs:"-" are NOT in the map
49+
if _, exists := m["shard_level"]; exists {
50+
t.Error("shard_level should not be in the config map")
51+
}
52+
}
53+
54+
func TestAddFlags(t *testing.T) {
55+
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
56+
opt := Options{
57+
FsName: "test-fs",
58+
}
59+
60+
opt.AddFlags(fs)
61+
62+
// Verify flags were registered
63+
f := fs.Lookup("fs-name")
64+
if f == nil {
65+
t.Fatal("flag 'fs-name' not found")
66+
}
67+
if f.DefValue != "test-fs" {
68+
t.Errorf("expected default value 'test-fs', got '%s'", f.DefValue)
69+
}
70+
71+
// Test parsing a flag
72+
err := fs.Parse([]string{"--fs-name", "overridden", "--shard-level", "3"})
73+
if err != nil {
74+
t.Fatalf("failed to parse flags: %v", err)
75+
}
76+
77+
if opt.FsName != "overridden" {
78+
t.Errorf("expected FsName 'overridden', got '%s'", opt.FsName)
79+
}
80+
if opt.ShardLevel != 3 {
81+
t.Errorf("expected ShardLevel 3, got %d", opt.ShardLevel)
82+
}
83+
}

0 commit comments

Comments
 (0)