Skip to content

Commit ba89c1f

Browse files
authored
External zip list file and updated docs (#18)
* New GET download endpoint for hosted manifest files * Update README, better docs, Spell check is a good thing.
1 parent 96fa7e3 commit ba89c1f

File tree

3 files changed

+95
-24
lines changed

3 files changed

+95
-24
lines changed

Diff for: README.md

+45-18
Original file line numberDiff line numberDiff line change
@@ -12,39 +12,65 @@ Highlights include:
1212
- Low memory: the files are streamed out to the client immediately
1313
- Low CPU: the default server doesn't compress files, only packages them into a zip, so there's minimal CPU load (configurable)
1414
- High concurrency: the two properties above allow a single small server to stream hundreds of large zips simultaneous
15-
- It includes a HTTP server, but can be used as a library (see zip_streamer.go).
15+
- It includes a HTTP server, but can be used as a library (see `zip_streamer.go`)
1616

17-
## HTTP Endpoints
17+
## JSON Zip File Descriptor
1818

19-
**POST /download**
19+
Each HTTP endpoint requires a JSON description of the desired zip file.
2020

21-
This endpoint takes a post, and returns a zip file.
21+
The JSON format root object should have an "entries" property, which holds an array of the files. Each entry requires 2 properties:
2222

23-
It expects a JSON body defining which files to include in the zip. The `ZipPath` is the path and filename in the resulting zip file (it should be a relative path).
23+
- `Url` REQUIRED: the URL of the file to include in the zip. Zipstreamer will fetch this via a GET request. The file must be public; if it is private, most file hosts provide query string authentication options for private files, which work well with Zipstreamer (example [AWS S3 Docs](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html)).
24+
- `ZipPath` REQUIRED: the path and filename where this entry should appear in the resulting zip file. This is a relative path to the root of the zip file.
2425

25-
Example body:
26+
Example JSON description with 2 files:
2627

2728
```
2829
{
2930
"entries": [
30-
{"Url":"https://server.com/image1.jpg","ZipPath":"image1.jpg"},
31-
{"Url":"https://server.com/image2.jpg","ZipPath":"in-a-sub-folder/image2.jpg"}
31+
{
32+
"Url":"https://server.com/image1.jpg",
33+
"ZipPath":"image1.jpg"
34+
},
35+
{
36+
"Url":"https://server.com/image2.jpg",
37+
"ZipPath":"in-a-sub-folder/image2.jpg"
38+
}
3239
]
3340
}
3441
```
3542

36-
**POST /create_download_link**
43+
## HTTP Endpoints
3744

38-
This endpoint creates a temporary link which can be used to download a zip via a GET. This is helpful as on a webapp it can be painful to POST results, and trigger a "Save File" popup with the result. With this, you can create the link in a POST, then open the download link in a new window.
45+
### POST /download
3946

40-
*Important*:
47+
This endpoint takes a http POST body containing the JSON description of the desired zip file, and returns a zip file.
48+
49+
### GET /download
50+
51+
Returns a zip file, from a JSON zip description hosted on another server. This is useful over the POST endpoint in a few use cases:
52+
53+
- You want to hide from the client where the original files are hosted (see zsid parameter)
54+
- Use cases where POST requests aren't easy to adopt (traditional static webpages)
55+
- You want to trigger a browsers' "Save File" UI, which isn't shown for POST requests. See `POST /create_download_link` as an alternative if you prefer writing this logic client side.
4156

42-
- These links are only live for 60 seconds. They are expected to be used immediately and are not long living.
43-
- This stores the link in an in memory cache, so it's not suitable for deploying to a multi-server cluster without extra configuration. If you are hosting a multi-server cluster make sure to enable Session Affinity on your host, so that requests from a given client are routed to a consistent correct host. See the deploy section for details on Heroku and Google Cloud Run.
57+
This endpoint requires one of two query parameters describing where to find the JSON descriptor. If both are provided, only `zsurl` will be used:
58+
59+
- `zsurl`: the full URL to the JSON file describing the zip. Example: `zipstreamer.yourserver.com/download?zsurl=https://gist.githubusercontent.com/scosman/449df713f97888b931c7b4e4f76f82b1/raw/82a1b54cd20ab44a916bd76a5b5d866acee2b29a/listfile.json`
60+
- `zsid`: must be used with the `ZS_LISTFILE_URL_PREFIX` environment variable. The JSON file will be fetched from `ZS_LISTFILE_URL_PREFIX + zsid`. This allows you to hide the full URL path from clients, revealing only the end of the URL. Example: `ZS_LISTFILE_URL_PREFIX = "https://gist.githubusercontent.com/scosman/"` and `zipstreamer.yourserver.com/download?zsid=449df713f97888b931c7b4e4f76f82b1/raw/82a1b54cd20ab44a916bd76a5b5d866acee2b29a/listfile.json`
61+
62+
### POST /create_download_link
63+
64+
This endpoint takes the JSON zip description in the POST body, stores it in a local cache, allowing the caller to fetch the zip file via an additional call to `GET /download_link/{link_id}`.
65+
66+
This is useful for if you want to trigger a browser "Save File" UI, which isn't shown for POST requests. See `GET /download` if you prefer a server-driven approach.
67+
68+
*Important*:
4469

45-
It expects the same body format as `/download`.
70+
- These links only live for 60 seconds. They are expected to be used immediately.
71+
- This stores the link in an in-memory cache, so it's not suitable for deploying to a multi-server cluster without extra configuration. If you are hosting a multi-server cluster, see the deployment section for options.
4672

47-
Here is an example response body:
73+
Here is an example response body containing the link ID. See docs for `GET /download_link/{link_id}` below for how to fetch the zip file:
4874

4975
```
5076
{
@@ -53,7 +79,7 @@ Here is an example response body:
5379
}
5480
```
5581

56-
**GET /download_link/{link_id}**
82+
### GET /download_link/{link_id}
5783

5884
Call this endpoint with a `link_id` generated with `/create_download_link` to download that zip file.
5985

@@ -89,9 +115,9 @@ docker build --tag docker-zipstreamer .
89115
docker run --env PORT=8080 -p 8080:8080 docker-zipstreamer
90116
```
91117

92-
#### Run Offical Package from Github Packages
118+
#### Run Official Package from Github Packages
93119

94-
Currently every change to master it published as a package. To use these offical packages:
120+
Currently every change to master it published as a package. To use these official packages:
95121

96122
```
97123
docker pull ghcr.io/scosman/packages/zipstreamer:latest
@@ -106,6 +132,7 @@ These ENV vars can be used to config the server:
106132
- `PORT` - Defaults to 4008. Sets which port the HTTP server binds to.
107133
- `ZS_URL_PREFIX` - If set, requires that the URL of files downloaded start with this prefix. Useful to preventing others from using your server to serve their files.
108134
- `ZS_COMPRESSION` - Defaults to no compression. It's not universally known, but zip files can be uncompressed, and used as a simple packaging format (combined many files into one). Set to `DEFLATE` to use zip deflate compression. **WARNING - enabling compression uses CPU, and will greatly reduce throughput of server**. Note: for file formats already optimized for size (JPEGs, MP4s), zip compression will often increase the total zip file size.
135+
- `ZS_LISTFILE_URL_PREFIX` - See documentation for `GET /download`
109136

110137
## Why
111138

Diff for: main.go

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
func main() {
1616
zipServer := zip_streamer.NewServer()
1717
zipServer.Compression = (os.Getenv("ZS_COMPRESSION") == "DEFLATE")
18+
zipServer.ListfileUrlPrefix = os.Getenv("ZS_LISTFILE_URL_PREFIX")
1819

1920
port := os.Getenv("PORT")
2021
if port == "" {

Diff for: zip_streamer/server.go

+49-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package zip_streamer
33
import (
44
"archive/zip"
55
"encoding/json"
6+
"errors"
67
"io/ioutil"
78
"net/http"
89
"time"
@@ -40,9 +41,10 @@ func UnmarshalPayload(payload []byte) ([]*FileEntry, error) {
4041
}
4142

4243
type Server struct {
43-
router *mux.Router
44-
linkCache LinkCache
45-
Compression bool
44+
router *mux.Router
45+
linkCache LinkCache
46+
Compression bool
47+
ListfileUrlPrefix string
4648
}
4749

4850
func NewServer() *Server {
@@ -56,6 +58,7 @@ func NewServer() *Server {
5658
}
5759

5860
r.HandleFunc("/download", server.HandlePostDownload).Methods("POST")
61+
r.HandleFunc("/download", server.HandleGetDownload).Methods("GET")
5962
r.HandleFunc("/create_download_link", server.HandleCreateLink).Methods("POST")
6063
r.HandleFunc("/download_link/{link_id}", server.HandleDownloadLink).Methods("GET")
6164

@@ -105,7 +108,47 @@ func (s *Server) HandlePostDownload(w http.ResponseWriter, req *http.Request) {
105108
return
106109
}
107110

108-
s.streamEntries(fileEntries, w, req)
111+
s.streamEntries(fileEntries, w)
112+
}
113+
114+
func (s *Server) HandleGetDownload(w http.ResponseWriter, req *http.Request) {
115+
params := req.URL.Query()
116+
listfileUrl := params.Get("zsurl")
117+
listFileId := params.Get("zsid")
118+
if listfileUrl == "" && s.ListfileUrlPrefix != "" && listFileId != "" {
119+
listfileUrl = s.ListfileUrlPrefix + listFileId
120+
}
121+
if listfileUrl == "" {
122+
w.WriteHeader(http.StatusBadRequest)
123+
w.Write([]byte(`{"status":"error","error":"invalid parameters"}`))
124+
return
125+
}
126+
127+
fileEntries, err := retrieveFileEntriesFromUrl(listfileUrl)
128+
if err != nil {
129+
w.WriteHeader(http.StatusNotFound)
130+
w.Write([]byte(`{"status":"error","error":"file not found"}`))
131+
return
132+
}
133+
134+
s.streamEntries(fileEntries, w)
135+
}
136+
137+
func retrieveFileEntriesFromUrl(listfileUrl string) ([]*FileEntry, error) {
138+
listfileResp, err := http.Get(listfileUrl)
139+
if err != nil {
140+
return nil, err
141+
}
142+
defer listfileResp.Body.Close()
143+
if listfileResp.StatusCode != http.StatusOK {
144+
return nil, errors.New("List File Server Error")
145+
}
146+
body, err := ioutil.ReadAll(listfileResp.Body)
147+
if err != nil {
148+
return nil, err
149+
}
150+
151+
return UnmarshalPayload(body)
109152
}
110153

111154
func (s *Server) HandleDownloadLink(w http.ResponseWriter, req *http.Request) {
@@ -117,10 +160,10 @@ func (s *Server) HandleDownloadLink(w http.ResponseWriter, req *http.Request) {
117160
return
118161
}
119162

120-
s.streamEntries(fileEntries, w, req)
163+
s.streamEntries(fileEntries, w)
121164
}
122165

123-
func (s *Server) streamEntries(fileEntries []*FileEntry, w http.ResponseWriter, req *http.Request) {
166+
func (s *Server) streamEntries(fileEntries []*FileEntry, w http.ResponseWriter) {
124167
zipStreamer, err := NewZipStream(fileEntries, w)
125168
if err != nil {
126169
w.WriteHeader(http.StatusBadRequest)

0 commit comments

Comments
 (0)