@@ -2,12 +2,13 @@ package cli
22
33import (
44 "context"
5- "errors"
65 "fmt"
76 "log"
87 "net/http"
98 "os"
109 "path/filepath"
10+ "slices"
11+ "strings"
1112 "time"
1213
1314 "log/slog"
@@ -17,9 +18,11 @@ import (
1718 "github.com/aws/aws-sdk-go-v2/config"
1819 "github.com/aws/aws-sdk-go-v2/credentials"
1920 "github.com/aws/aws-sdk-go-v2/service/s3"
21+ "github.com/pkg/errors"
2022 "github.com/readium/cli/pkg/serve"
2123 "github.com/readium/cli/pkg/serve/client"
2224 "github.com/readium/go-toolkit/pkg/streamer"
25+ "github.com/readium/go-toolkit/pkg/util/url"
2326 "github.com/spf13/cobra"
2427 "google.golang.org/api/option"
2528)
@@ -30,6 +33,10 @@ var bindAddressFlag string
3033
3134var bindPortFlag uint16
3235
36+ var schemeFlag []string
37+
38+ var fileDirectoryFlag string
39+
3340// Cloud-related flags
3441var s3EndpointFlag string
3542var s3RegionFlag string
@@ -45,28 +52,29 @@ var remoteArchiveCacheCount uint32
4552var remoteArchiveCacheAll uint32
4653
4754var serveCmd = & cobra.Command {
48- Use : "serve <directory> " ,
55+ Use : "serve" ,
4956 Short : "Start a local HTTP server, serving a specified directory of publications" ,
5057 Long : `Start a local HTTP server, serving a specified directory of publications.
5158
5259This command will start an HTTP serve listening by default on 'localhost:15080',
53- serving all compatible files (EPUB, PDF, CBZ, etc.) found in the directory
60+ serving all compatible files (EPUB, PDF, CBZ, etc.) available from the enabled
61+ access schemes (file, http, https, s3, gs, or a local path if file scheme is enabled)
5462as Readium Web Publications. To get started, the manifest can be accessed from
5563'http://localhost:15080/<filename in base64url encoding without padding>/manifest.json'.
5664This file serves as the entry point and contains metadata and links to the rest
5765of the files that can be accessed for the publication.
5866
59- For debugging purposes, the server also exposes a '/list.json' endpoint that
60- returns a list of all the publications found in the directory along with their
61- encoded paths. This will be replaced by an OPDS 2 feed in a future release.
67+ If local file access is enabled, the server also exposes a '/list.json' endpoint that,
68+ for debugging purposes, returns a list of all the publications found in the directory
69+ along with their encoded paths. This will be replaced by an OPDS 2 feed (or similar)
70+ in a future release.
6271
63- Note: This server is not meant for production usage, and should not be exposed
64- to the internet except for testing/debugging purposes .` ,
72+ Note: Take caution before exposing this server on the internet. It does not
73+ implement any authentication, and may have more access to files than expected .` ,
6574 Args : func (cmd * cobra.Command , args []string ) error {
66- if len (args ) == 0 {
67- return errors .New ("expects a directory path to serve publications from" )
68- } else if len (args ) > 1 {
69- return errors .New ("accepts a directory path" )
75+ if len (args ) > 0 {
76+ // For users migrating from previous versions of the CLI
77+ return errors .New ("no arguments expected, base directory for local files is now set with a flag" )
7078 }
7179 return nil
7280 },
@@ -78,16 +86,37 @@ to the internet except for testing/debugging purposes.`,
7886 // occurs.
7987 cmd .SilenceUsage = true
8088
81- path := filepath .Clean (args [0 ])
82- fi , err := os .Stat (path )
83- if err != nil {
84- if os .IsNotExist (err ) {
85- return fmt .Errorf ("given directory %s does not exist" , path )
89+ // Validate schemes
90+ schemes := make ([]url.Scheme , len (schemeFlag ))
91+ for i , v := range schemeFlag {
92+ lowerScheme := url .Scheme (strings .ToLower (v )) // Accomodate for wrong capitalization
93+ switch lowerScheme {
94+ case url .SchemeFile , url .SchemeHTTP , url .SchemeHTTPS , url .SchemeS3 , url .SchemeGS :
95+ schemes [i ] = lowerScheme
96+ default :
97+ return fmt .Errorf ("invalid scheme %q, acceptable values: file, http, https, s3, gs" , v )
8698 }
87- return fmt .Errorf ("failed to stat %s: %w" , path , err )
8899 }
89- if ! fi .IsDir () {
90- return fmt .Errorf ("given path %s is not a directory" , path )
100+
101+ if fileDirectoryFlag != "" {
102+ if ! slices .Contains (schemes , url .SchemeFile ) {
103+ slog .Warn ("local directory specified, but file scheme is not enabled" )
104+ }
105+
106+ path := filepath .Clean (fileDirectoryFlag )
107+ fi , err := os .Stat (path )
108+ if err != nil {
109+ if os .IsNotExist (err ) {
110+ return fmt .Errorf ("given directory %s does not exist" , path )
111+ }
112+ return fmt .Errorf ("failed to stat %s: %w" , path , err )
113+ }
114+ if ! fi .IsDir () {
115+ return fmt .Errorf ("given path %s is not a directory" , path )
116+ }
117+ fileDirectoryFlag = path
118+ } else if slices .Contains (schemes , url .SchemeFile ) {
119+ return fmt .Errorf ("file scheme is enabled, but no local directory was specified with the --file-directory flag" )
91120 }
92121
93122 // Log level
@@ -98,51 +127,67 @@ to the internet except for testing/debugging purposes.`,
98127 }
99128
100129 // Set up remote publication retrieval clients
101- remote := serve.Remote {}
130+ remote := serve.Remote {
131+ LocalDirectory : fileDirectoryFlag ,
132+ }
133+
134+ // 30 seconds to set up remote retrieval clients
135+ ctx , cancel := context .WithTimeout (context .Background (), 30 * time .Second )
136+ defer cancel ()
102137
103138 // S3
104- options := []func (* config.LoadOptions ) error {
105- config .WithRegion (s3RegionFlag ),
106- config .WithRequestChecksumCalculation (0 ),
107- config .WithResponseChecksumValidation (0 ),
108- // TODO: look into custom HTTP client, user-agent
109- }
110- if s3AccessKeyFlag != "" && s3SecretKeyFlag != "" {
111- options = append (options , config .WithCredentialsProvider (credentials .NewStaticCredentialsProvider (s3AccessKeyFlag , s3SecretKeyFlag , "" )))
112- }
113- cfg , err := config .LoadDefaultConfig (context .Background (), options ... )
114- if err != nil {
115- log .Fatal (err )
116- }
117- _ , err = cfg .Credentials .Retrieve (context .Background ())
118- if err == nil {
119- remote .S3 = s3 .NewFromConfig (cfg , func (o * s3.Options ) {
120- if s3EndpointFlag != "" {
121- o .BaseEndpoint = aws .String (s3EndpointFlag )
122- }
123- o .DisableLogOutputChecksumValidationSkipped = true // Non-AWS S3 tends not to support this and it causes logspam
124- o .UsePathStyle = s3UsePathStyleFlag
125- })
126- } else {
127- slog .Warn ("S3 credentials retrieval failed, S3 support will be disabled" , "error" , err )
139+ if slices .Contains (schemes , url .SchemeS3 ) {
140+ options := []func (* config.LoadOptions ) error {
141+ config .WithRegion (s3RegionFlag ),
142+ config .WithRequestChecksumCalculation (0 ),
143+ config .WithResponseChecksumValidation (0 ),
144+ // TODO: look into custom HTTP client, user-agent
145+ }
146+ if s3AccessKeyFlag != "" && s3SecretKeyFlag != "" {
147+ options = append (options , config .WithCredentialsProvider (credentials .NewStaticCredentialsProvider (s3AccessKeyFlag , s3SecretKeyFlag , "" )))
148+ }
149+ cfg , err := config .LoadDefaultConfig (ctx , options ... )
150+ if err != nil {
151+ log .Fatal (err )
152+ }
153+ _ , err = cfg .Credentials .Retrieve (ctx )
154+ if err == nil {
155+ remote .S3 = s3 .NewFromConfig (cfg , func (o * s3.Options ) {
156+ if s3EndpointFlag != "" {
157+ o .BaseEndpoint = aws .String (s3EndpointFlag )
158+ }
159+ o .DisableLogOutputChecksumValidationSkipped = true // Non-AWS S3 tends not to support this and it causes logspam
160+ o .UsePathStyle = s3UsePathStyleFlag
161+ })
162+ } else {
163+ return fmt .Errorf ("S3 credentials retrieval failed: %w" , err )
164+ }
165+ } else if s3AccessKeyFlag != "" || s3SecretKeyFlag != "" || s3EndpointFlag != "" {
166+ slog .Warn ("S3-related flags are set, but S3 scheme is not enabled" )
128167 }
129168
130169 // GCS
131- opts := []option.ClientOption {
132- option .WithScopes (storage .ScopeReadOnly ),
133- storage .WithJSONReads (),
134- // option.WithUserAgent(TODO),
135- // TODO: look into more efficient transport (HTTP client)
136- }
137- remote .GCS , err = storage .NewClient (context .Background (), opts ... )
138- if err != nil {
139- slog .Warn ("GCS client creation failed, GCS support will be disabled" , "error" , err )
170+ var err error
171+ if slices .Contains (schemes , url .SchemeGS ) {
172+ opts := []option.ClientOption {
173+ option .WithScopes (storage .ScopeReadOnly ),
174+ storage .WithJSONReads (),
175+ // option.WithUserAgent(TODO),
176+ // TODO: look into more efficient transport (HTTP client)
177+ }
178+ remote .GCS , err = storage .NewClient (ctx , opts ... )
179+ if err != nil {
180+ return fmt .Errorf ("GCS client creation failed: %w" , err )
181+ }
140182 }
141183
184+ // HTTP/HTTPS
142185 remote .HTTP , err = client .NewHTTPClient (httpAuthorizationFlag )
143186 if err != nil {
144187 slog .Warn ("HTTP client creation failed, HTTP support will be disabled" , "error" , err )
145188 }
189+ remote .HTTPEnabled = slices .Contains (schemes , url .SchemeHTTP )
190+ remote .HTTPSEnabled = slices .Contains (schemes , url .SchemeHTTPS )
146191
147192 // Remote archive streaming tweaks
148193 remote .Config .CacheCountThreshold = int64 (remoteArchiveCacheCount )
@@ -153,7 +198,6 @@ to the internet except for testing/debugging purposes.`,
153198 // Create server
154199 pubServer := serve .NewServer (serve.ServerConfig {
155200 Debug : debugFlag ,
156- BaseDirectory : path ,
157201 JSONIndent : indentFlag ,
158202 InferA11yMetadata : streamer .InferA11yMetadata (inferA11yFlag ),
159203 }, remote )
@@ -180,12 +224,15 @@ to the internet except for testing/debugging purposes.`,
180224func init () {
181225 rootCmd .AddCommand (serveCmd )
182226
227+ serveCmd .Flags ().StringSliceVarP (& schemeFlag , "scheme" , "s" , []string {"file" }, "Scheme(s) to enable for accessing content. Acceptable values: file, http, https, s3, gs" )
183228 serveCmd .Flags ().StringVarP (& bindAddressFlag , "address" , "a" , "localhost" , "Address to bind the HTTP server to" )
184229 serveCmd .Flags ().Uint16VarP (& bindPortFlag , "port" , "p" , 15080 , "Port to bind the HTTP server to" )
185230 serveCmd .Flags ().StringVarP (& indentFlag , "indent" , "i" , "" , "Indentation used to pretty-print JSON files" )
186231 serveCmd .Flags ().Var (& inferA11yFlag , "infer-a11y" , "Infer accessibility metadata: no, merged, split" )
187232 serveCmd .Flags ().BoolVarP (& debugFlag , "debug" , "d" , false , "Enable debug mode" )
188233
234+ serveCmd .Flags ().StringVar (& fileDirectoryFlag , "file-directory" , "" , "Local directory path to serve publications from" )
235+
189236 serveCmd .Flags ().StringVar (& s3EndpointFlag , "s3-endpoint" , "" , "Custom S3 endpoint URL" )
190237 serveCmd .Flags ().StringVar (& s3RegionFlag , "s3-region" , "auto" , "S3 region" )
191238 serveCmd .Flags ().StringVar (& s3AccessKeyFlag , "s3-access-key" , "" , "S3 access key" )
0 commit comments