3333// - encrypt (optional): Enable AES-GCM encryption ("on" or "aesgcm")
3434// - encrypt_key (optional): Base64-encoded AES key (URL-safe, RFC 4648 §5)
3535// - update_mtime (optional): Update file mtime on cache hits ("on" to enable)
36+ // - umask (optional): Permission mask to apply to created files and directories (default 0)
3637//
3738// # Usage Examples
3839//
5859// fscache://?appname=myapp&update_mtime=on
5960// fscache.Open("myapp", fscache.WithUpdateMTime(true))
6061//
62+ // Private cache files and directories:
63+ //
64+ // fscache://?appname=myapp&umask=077
65+ // fscache.Open("myapp", fscache.WithUmask(0o077))
66+ //
67+ // On Windows, only umask=0 is supported.
68+ //
6169// # Encryption Key Management
6270//
6371// Encryption keys can be provided via DSN parameter or environment variable:
@@ -91,7 +99,9 @@ import (
9199 "net/url"
92100 "os"
93101 "path/filepath"
102+ "runtime"
94103 "slices"
104+ "strconv"
95105 "strings"
96106 "time"
97107
@@ -141,6 +151,7 @@ type fsCache struct {
141151 timeout time.Duration // optional timeout for operations
142152 enc encryptor // optional encryptor for data
143153 updateMTime bool // whether to update file mtime on cache hits
154+ umask fs.FileMode // umask for created files and directories
144155
145156 // internal dependencies
146157
@@ -162,6 +173,17 @@ func parseTimeout(v string) time.Duration {
162173 return max (timeout , 0 )
163174}
164175
176+ func parseUmask (v string ) (fs.FileMode , error ) {
177+ if v == "" {
178+ return fs .FileMode (0 ), errors .New ("empty umask" )
179+ }
180+ umask , err := strconv .ParseUint (v , 8 , 32 )
181+ if err != nil {
182+ return fs .FileMode (0 ), fmt .Errorf ("invalid umask: %s: %w" , v , err )
183+ }
184+ return fs .FileMode (umask ), nil
185+ }
186+
165187var errEncryptionEnabledWithoutKey = errors .New ("fscache: encryption enabled but no key provided" )
166188
167189type Option interface {
@@ -219,28 +241,51 @@ func WithUpdateMTime(enabled bool) Option {
219241 })
220242}
221243
244+ // WithUmask sets the permission mask for created files and directories. On
245+ // Windows, only 0 (no permission change) is supported, see [os.Chmod].
246+ func WithUmask (umask fs.FileMode ) Option {
247+ return optionFunc (func (c * fsCache ) error {
248+ if umask > 0o777 {
249+ return fmt .Errorf ("%o: invalid umask" , umask )
250+ }
251+ if runtime .GOOS == "windows" && umask != 0 {
252+ return fmt .Errorf ("%o: unsupported umask on Windows" , umask )
253+ }
254+ c .umask = umask
255+ return nil
256+ })
257+ }
258+
222259func fromURL (u * url.URL ) (* fsCache , error ) {
223- appname := u .Query ().Get ("appname" )
260+ query := u .Query ()
261+ appname := query .Get ("appname" )
224262 if appname == "" {
225263 return nil , ErrMissingAppName
226264 }
227265 opts := make ([]Option , 0 , 5 )
228266 if u .Path != "" && u .Path != "/" {
229267 opts = append (opts , WithBaseDir (u .Path ))
230268 }
231- if v := u . Query () .Get ("connect_timeout" ); v != "" {
269+ if v := query .Get ("connect_timeout" ); v != "" {
232270 opts = append (opts , WithConnectTimeout (parseTimeout (v )))
233271 }
234- if v := u . Query () .Get ("timeout" ); v != "" {
272+ if v := query .Get ("timeout" ); v != "" {
235273 opts = append (opts , WithTimeout (parseTimeout (v )))
236274 }
237- if encrypt := u . Query () .Get ("encrypt" ); encrypt == "on" || encrypt == "aesgcm" {
238- key := cmp .Or (u . Query () .Get ("encrypt_key" ), os .Getenv ("FSCACHE_ENCRYPT_KEY" ))
275+ if encrypt := query .Get ("encrypt" ); encrypt == "on" || encrypt == "aesgcm" {
276+ key := cmp .Or (query .Get ("encrypt_key" ), os .Getenv ("FSCACHE_ENCRYPT_KEY" ))
239277 opts = append (opts , WithEncryption (key ))
240278 }
241- if updateMTime := u . Query () .Get ("update_mtime" ); updateMTime == "on" {
279+ if updateMTime := query .Get ("update_mtime" ); updateMTime == "on" {
242280 opts = append (opts , WithUpdateMTime (true ))
243281 }
282+ if query .Has ("umask" ) {
283+ umask , err := parseUmask (query .Get ("umask" ))
284+ if err != nil {
285+ return nil , err
286+ }
287+ opts = append (opts , WithUmask (umask ))
288+ }
244289 if cap (opts ) > len (opts ) {
245290 opts = slices .Clip (opts )
246291 }
@@ -291,7 +336,7 @@ func (c *fsCache) initialize(appname string) error {
291336 return ErrMissingAppName
292337 }
293338 c .base = filepath .Join (c .base , appname )
294- if err := os .MkdirAll (c .base , 0o755 ); err != nil {
339+ if err := os .MkdirAll (c .base , 0o755 &^ c . umask ); err != nil {
295340 return errors .Join (ErrCreateCacheDir , err )
296341 }
297342 var err error
@@ -409,14 +454,23 @@ func (c *fsCache) set(key string, entry []byte) error {
409454 }
410455 }
411456 name := c .fn .FileName (key )
412- if err := c .root .MkdirAll (filepath .Dir (name ), 0o755 ); err != nil {
457+ if err := c .root .MkdirAll (filepath .Dir (name ), 0o755 &^ c . umask ); err != nil {
413458 return err
414459 }
415460 f , err := c .root .Create (name )
416461 if err != nil {
417462 return err
418463 }
419464 defer f .Close ()
465+ if c .umask != 0 {
466+ info , err2 := f .Stat ()
467+ if err2 != nil {
468+ return err2
469+ }
470+ if err3 := f .Chmod (info .Mode ().Perm () &^ c .umask ); err3 != nil {
471+ return err3
472+ }
473+ }
420474 _ , err = f .Write (entry )
421475 if err != nil {
422476 return err
0 commit comments