@@ -18,7 +18,10 @@ package browse
1818
1919import (
2020 "bytes"
21+ "compress/flate"
2122 "encoding/json"
23+ "fmt"
24+ "io"
2225 "net/http"
2326 "net/url"
2427 "os"
@@ -29,9 +32,11 @@ import (
2932 "text/template"
3033 "time"
3134
35+ "github.com/dustin/go-humanize"
36+ "github.com/mholt/archiver/v3"
37+ "github.com/rakyll/statik/fs"
3238 "github.com/tmpim/casket/caskethttp/httpserver"
3339 "github.com/tmpim/casket/caskethttp/staticfiles"
34- "github.com/dustin/go-humanize"
3540)
3641
3742const (
@@ -41,6 +46,35 @@ const (
4146 sortByTime = "time"
4247)
4348
49+ type ArchiveType string
50+
51+ const (
52+ ArchiveZip ArchiveType = "zip"
53+ ArchiveTar ArchiveType = "tar"
54+ ArchiveTarGz ArchiveType = "tar.gz"
55+ ArchiveTarXz ArchiveType = "tar.xz"
56+ ArchiveTarBrotli ArchiveType = "tar.br"
57+ ArchiveTarBz2 ArchiveType = "tar.bz2"
58+ ArchiveTarLz4 ArchiveType = "tar.lz4"
59+ ArchiveTarSz ArchiveType = "tar.sz"
60+ ArchiveTarZstd ArchiveType = "tar.zst"
61+ )
62+
63+ var (
64+ ArchiveTypes = []ArchiveType {ArchiveZip , ArchiveTar , ArchiveTarGz , ArchiveTarXz , ArchiveTarBrotli , ArchiveTarBz2 , ArchiveTarLz4 , ArchiveTarSz , ArchiveTarZstd }
65+ ArchiveTypeToMime = map [ArchiveType ]string {
66+ ArchiveZip : "application/zip" ,
67+ ArchiveTar : "application/tar" ,
68+ ArchiveTarGz : "application/tar+gzip" ,
69+ ArchiveTarXz : "application/tar+xz" ,
70+ ArchiveTarBrotli : "application/tar+brotli" ,
71+ ArchiveTarBz2 : "application/tar+bzip2" ,
72+ ArchiveTarLz4 : "application/tar+lz4" ,
73+ ArchiveTarSz : "application/tar+snappy" ,
74+ ArchiveTarZstd : "application/tar+zstd" ,
75+ }
76+ )
77+
4478// Browse is an http.Handler that can show a file listing when
4579// directories in the given paths are specified.
4680type Browse struct {
@@ -51,10 +85,11 @@ type Browse struct {
5185
5286// Config is a configuration for browsing in a particular path.
5387type Config struct {
54- PathScope string // the base path the URL must match to enable browsing
55- Fs staticfiles.FileServer
56- Variables interface {}
57- Template * template.Template
88+ PathScope string // the base path the URL must match to enable browsing
89+ Fs staticfiles.FileServer
90+ Variables interface {}
91+ Template * template.Template
92+ ArchiveTypes []ArchiveType
5893}
5994
6095// A Listing is the context used to fill out a template.
@@ -86,6 +121,8 @@ type Listing struct {
86121 // If ≠0 then Items have been limited to that many elements.
87122 ItemsLimitedTo int
88123
124+ ArchiveTypes []ArchiveType
125+
89126 // Optional custom variables for use in browse templates.
90127 User interface {}
91128
@@ -286,12 +323,13 @@ func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, config
286323 }
287324
288325 return Listing {
289- Name : path .Base (urlPath ),
290- Path : urlPath ,
291- CanGoUp : canGoUp ,
292- Items : fileInfos ,
293- NumDirs : dirCount ,
294- NumFiles : fileCount ,
326+ Name : path .Base (urlPath ),
327+ Path : urlPath ,
328+ CanGoUp : canGoUp ,
329+ Items : fileInfos ,
330+ NumDirs : dirCount ,
331+ NumFiles : fileCount ,
332+ ArchiveTypes : config .ArchiveTypes ,
295333 }, hasIndexFile
296334}
297335
@@ -387,7 +425,7 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
387425 return http .StatusMovedPermanently , nil
388426 }
389427
390- return b .ServeListing (w , r , requestedFilepath , bc )
428+ return b .ServeListing (w , r , requestedFilepath , info , bc )
391429}
392430
393431func (b Browse ) loadDirectoryContents (requestedFilepath http.File , urlPath string , config * Config ) (* Listing , bool , error ) {
@@ -451,7 +489,7 @@ func (b Browse) handleSortOrder(w http.ResponseWriter, r *http.Request, scope st
451489}
452490
453491// ServeListing returns a formatted view of 'requestedFilepath' contents'.
454- func (b Browse ) ServeListing (w http.ResponseWriter , r * http.Request , requestedFilepath http.File , bc * Config ) (int , error ) {
492+ func (b Browse ) ServeListing (w http.ResponseWriter , r * http.Request , requestedFilepath http.File , info os. FileInfo , bc * Config ) (int , error ) {
455493 listing , containsIndex , err := b .loadDirectoryContents (requestedFilepath , r .URL .Path , bc )
456494 if err != nil {
457495 switch {
@@ -473,6 +511,20 @@ func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFi
473511 }
474512 listing .User = bc .Variables
475513
514+ // Check if this is an archive request
515+ archiveTypeStr := r .URL .Query ().Get ("archive" )
516+ if archiveTypeStr != "" {
517+ archiveType := ArchiveType (archiveTypeStr )
518+ for _ , t := range bc .ArchiveTypes {
519+ if t == archiveType {
520+ return b .ServeArchive (w , r , path .Clean (r .URL .Path ), info , archiveType , bc )
521+ }
522+ }
523+
524+ // We cannot produce an archive of this type, return 404 Not Found
525+ return http .StatusNotFound , nil
526+ }
527+
476528 // Copy the query values into the Listing struct
477529 var limit int
478530 listing .Sort , listing .Order , limit , err = b .handleSortOrder (w , r , bc .PathScope )
@@ -509,6 +561,106 @@ func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFi
509561 return http .StatusOK , nil
510562}
511563
564+ func (b Browse ) ServeArchive (w http.ResponseWriter , r * http.Request , dirPath string , dirInfo os.FileInfo , archiveType ArchiveType , bc * Config ) (int , error ) {
565+ contentType := ArchiveTypeToMime [archiveType ]
566+
567+ fileBaseName := path .Base (dirPath )
568+ if fileBaseName == "/" {
569+ fileBaseName = "root"
570+ }
571+
572+ w .Header ().Set ("Content-Type" , contentType )
573+ w .Header ().Set ("Content-Disposition" , "attachment; filename=" + strconv .Quote (fileBaseName + "." + string (archiveType )))
574+
575+ writer := archiveType .GetWriter ()
576+ err := writer .Create (w )
577+ if err != nil {
578+ return http .StatusInternalServerError , err
579+ }
580+ defer writer .Close ()
581+
582+ err = fs .Walk (bc .Fs .Root , dirPath , func (path string , info os.FileInfo , err error ) error {
583+ if err != nil {
584+ return err
585+ }
586+
587+ if info == nil {
588+ return fmt .Errorf ("file info was nil" )
589+ }
590+
591+ if path == dirPath {
592+ return nil // Skip the containing directory
593+ }
594+
595+ var file io.ReadCloser
596+ if info .Mode ().IsRegular () {
597+ file , err = bc .Fs .Root .Open (path )
598+ if err != nil {
599+ return fmt .Errorf ("%s: opening: %v" , path , err )
600+ }
601+ defer file .Close ()
602+ }
603+
604+ archiveFileName , err := archiver .NameInArchive (dirInfo , dirPath , path )
605+ if err != nil {
606+ return err
607+ }
608+
609+ err = writer .Write (archiver.File {
610+ FileInfo : archiver.FileInfo {
611+ FileInfo : info ,
612+ CustomName : archiveFileName ,
613+ },
614+ ReadCloser : file ,
615+ })
616+ if err != nil {
617+ return fmt .Errorf ("writing file to archive: %v" , err )
618+ }
619+
620+ return nil
621+ })
622+
623+ if err != nil {
624+ return http .StatusInternalServerError , err
625+ }
626+
627+ // Returning 0 indicates we intend to stream the file
628+ return 0 , nil
629+ }
630+
631+ func (a ArchiveType ) GetWriter () archiver.Writer {
632+ switch a {
633+ case ArchiveZip :
634+ return & archiver.Zip {
635+ FileMethod : archiver .Deflate ,
636+ CompressionLevel : flate .DefaultCompression ,
637+ MkdirAll : true ,
638+ SelectiveCompression : true ,
639+ ImplicitTopLevelFolder : true ,
640+ }
641+
642+ case ArchiveTar :
643+ return & archiver.Tar {MkdirAll : true , ImplicitTopLevelFolder : true }
644+ case ArchiveTarGz :
645+ return & archiver.TarGz {Tar : & archiver.Tar {MkdirAll : true , ImplicitTopLevelFolder : true }, CompressionLevel : flate .DefaultCompression }
646+ case ArchiveTarXz :
647+ return & archiver.TarXz {Tar : & archiver.Tar {MkdirAll : true , ImplicitTopLevelFolder : true }}
648+ case ArchiveTarBrotli :
649+ return & archiver.TarBrotli {Tar : & archiver.Tar {MkdirAll : true , ImplicitTopLevelFolder : true }, Quality : 3 }
650+ case ArchiveTarBz2 :
651+ return & archiver.TarBz2 {Tar : & archiver.Tar {MkdirAll : true , ImplicitTopLevelFolder : true }, CompressionLevel : 2 }
652+ case ArchiveTarLz4 :
653+ return & archiver.TarLz4 {Tar : & archiver.Tar {MkdirAll : true , ImplicitTopLevelFolder : true }, CompressionLevel : 1 }
654+ case ArchiveTarSz :
655+ return & archiver.TarSz {Tar : & archiver.Tar {MkdirAll : true , ImplicitTopLevelFolder : true }}
656+ case ArchiveTarZstd :
657+ return & archiver.TarZstd {Tar : & archiver.Tar {MkdirAll : true , ImplicitTopLevelFolder : true }}
658+
659+ default :
660+ panic ("unknown archive type: " + a )
661+ }
662+ }
663+
512664func (b Browse ) formatAsJSON (listing * Listing , bc * Config ) (* bytes.Buffer , error ) {
513665 marsh , err := json .Marshal (listing .Items )
514666 if err != nil {
0 commit comments