diff options
Diffstat (limited to 'contrib/fs')
-rw-r--r-- | contrib/fs/file.go | 52 | ||||
-rw-r--r-- | contrib/fs/gemini.go | 109 | ||||
-rw-r--r-- | contrib/fs/gopher.go | 150 | ||||
-rw-r--r-- | contrib/fs/handlers.go | 162 | ||||
-rw-r--r-- | contrib/fs/spartan.go | 110 |
5 files changed, 189 insertions, 394 deletions
diff --git a/contrib/fs/file.go b/contrib/fs/file.go index 9f11f4f..4d79fea 100644 --- a/contrib/fs/file.go +++ b/contrib/fs/file.go @@ -1,36 +1,9 @@ package fs import ( - "mime" - "os" "strings" - "unicode/utf8" ) -func mediaType(filePath string) string { - if strings.HasSuffix(filePath, ".gmi") { - // This may not be present in the listings searched by mime.TypeByExtension, - // so provide a dedicated fast path for it here. - return "text/gemini" - } - - slashIdx := strings.LastIndex(filePath, "/") - dotIdx := strings.LastIndex(filePath[slashIdx+1:], ".") - if dotIdx == -1 { - return "application/octet-stream" - } - ext := filePath[slashIdx+1+dotIdx:] - - mtype := mime.TypeByExtension(ext) - if mtype == "" { - if contentsAreText(filePath) { - return "text/plain" - } - return "application/octet-stream" - } - return mtype -} - func isPrivate(fullpath string) bool { for _, segment := range strings.Split(fullpath, "/") { if len(segment) > 1 && segment[0] == '.' { @@ -39,28 +12,3 @@ func isPrivate(fullpath string) bool { } return false } - -func contentsAreText(filepath string) bool { - f, err := os.Open(filepath) - if err != nil { - return false - } - defer func() { _ = f.Close() }() - - var buf [1024]byte - n, err := f.Read(buf[:]) - if err != nil { - return false - } - - for i, c := range string(buf[:n]) { - if i+utf8.UTFMax > n { - // incomplete last char - break - } - if c == 0xFFFD || c < ' ' && c != '\n' && c != '\t' && c != '\f' { - return false - } - } - return true -} diff --git a/contrib/fs/gemini.go b/contrib/fs/gemini.go index 79dcc63..6f9c75d 100644 --- a/contrib/fs/gemini.go +++ b/contrib/fs/gemini.go @@ -7,7 +7,6 @@ import ( "net/url" "os" "path" - "path/filepath" "strings" "text/template" @@ -42,7 +41,7 @@ func TitanUpload(fsroot, urlroot string, approver tlsauth.Approver) sr.Middlewar if _, err := io.Copy(tmpf, body); err != nil { _ = os.Remove(tmpf.Name()) - return gemini.PermanentFailure(err) + return gemini.Failure(err) } request = cloneRequest(request) @@ -87,30 +86,7 @@ func cloneRequest(start *sr.Request) *sr.Request { // // It only serves responses for paths which do not correspond to directories on disk. func GeminiFileHandler(fsroot, urlroot string) sr.Handler { - fsroot = strings.TrimRight(fsroot, "/") - - return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { - if !strings.HasPrefix(request.Path, urlroot) { - return nil - } - requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/") - - fpath := filepath.Join(fsroot, requestpath) - if isPrivate(fpath) { - return nil - } - if isf, err := isFile(fpath); err != nil { - return gemini.Failure(err) - } else if !isf { - return nil - } - - file, err := os.Open(fpath) - if err != nil { - return gemini.Failure(err) - } - return gemini.Success(mediaType(fpath), file) - }) + return fileHandler(gemini.ServerProtocol, fsroot, urlroot) } // GeminiDirectoryDefault serves up default files for directory path requests. @@ -124,47 +100,7 @@ func GeminiFileHandler(fsroot, urlroot string) sr.Handler { // redirects to a URL with the trailing slash appended. This is necessary for relative // links in the directory's contents to function properly. func GeminiDirectoryDefault(fsroot, urlroot string, filenames ...string) sr.Handler { - fsroot = strings.TrimRight(fsroot, "/") - - return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { - if !strings.HasPrefix(request.Path, urlroot) { - return nil - } - - if !strings.HasSuffix(request.Path, "/") { - u := *request.URL - u.Path += "/" - return gemini.PermanentRedirect(u.String()) - } - - requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/") - fpath := filepath.Join(fsroot, requestpath) - if isPrivate(fpath) { - return nil - } - if isd, err := isDir(fpath); err != nil { - return gemini.Failure(err) - } else if !isd { - return nil - } - - for _, fname := range filenames { - candidatepath := filepath.Join(fpath, fname) - if isf, err := isFile(candidatepath); err != nil { - return gemini.Failure(err) - } else if !isf { - continue - } - - file, err := os.Open(candidatepath) - if err != nil { - return gemini.Failure(err) - } - return gemini.Success(mediaType(candidatepath), file) - } - - return nil - }) + return directoryDefault(gemini.ServerProtocol, fsroot, urlroot, true, filenames...) } // GeminiDirectoryListing produces a listing of the contents of any requested directories. @@ -177,40 +113,11 @@ func GeminiDirectoryDefault(fsroot, urlroot string, filenames ...string) sr.Hand // // The template may be nil, in which case DefaultGeminiDirectoryList is used instead. The // template is then processed with RenderDirectoryListing. -func GeminiDirectoryListing(fsroot, urlroot string, template *template.Template) sr.Handler { - fsroot = strings.TrimRight(fsroot, "/") - - return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { - if !strings.HasSuffix(request.Path, "/") { - u := *request.URL - u.Path += "/" - return gemini.PermanentRedirect(u.String()) - } - if !strings.HasPrefix(request.Path, urlroot) { - return nil - } - requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/") - - fpath := filepath.Join(fsroot, requestpath) - if isPrivate(fpath) { - return nil - } - if isd, err := isDir(fpath); err != nil { - return gemini.Failure(err) - } else if !isd { - return nil - } - - if template == nil { - template = DefaultGeminiDirectoryList - } - body, err := RenderDirectoryListing(fpath, requestpath, template, request.Server) - if err != nil { - return gemini.Failure(err) - } - - return gemini.Success("text/gemini", body) - }) +func GeminiDirectoryListing(fsroot, urlroot string, tmpl *template.Template) sr.Handler { + if tmpl == nil { + tmpl = DefaultGeminiDirectoryList + } + return directoryListing(gemini.ServerProtocol, fsroot, urlroot, "file.gmi", true, tmpl) } // DefaultGeminiDirectoryList is a template which renders a reasonable gemtext dir list. diff --git a/contrib/fs/gopher.go b/contrib/fs/gopher.go index 0a0b482..209a4ec 100644 --- a/contrib/fs/gopher.go +++ b/contrib/fs/gopher.go @@ -2,9 +2,6 @@ package fs import ( "context" - "os" - "path/filepath" - "slices" "strings" sr "tildegit.org/tjp/sliderule" @@ -16,50 +13,8 @@ import ( // // It only serves responses for paths which correspond to files, not directories. func GopherFileHandler(fsroot, urlroot string, settings *gophermap.FileSystemSettings) sr.Handler { - fsroot = strings.TrimRight(fsroot, "/") - - return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { - if !strings.HasPrefix(request.Path, urlroot) { - return nil - } - requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/") - - path := filepath.Join(fsroot, requestpath) - if isPrivate(path) { - return nil - } - if isf, err := isFile(path); err != nil { - return gopher.Error(err).Response() - } else if !isf { - return nil - } - - if settings == nil { - settings = &gophermap.FileSystemSettings{} - } - - file, err := os.Open(path) - if err != nil { - return gopher.Error(err).Response() - } - - if !(settings.ParseExtended && isMap(path, *settings)) { - return gopher.File(gopher.GuessItemType(path), file) - } - - defer func() { _ = file.Close() }() - - edoc, err := gophermap.ParseExtended(file, request.URL) - if err != nil { - return gopher.Error(err).Response() - } - - doc, _, err := edoc.Compatible(filepath.Dir(path), *settings) - if err != nil { - return gopher.Error(err).Response() - } - return doc.Response() - }) + handler := fileHandler(gopher.ServerProtocol, fsroot, urlroot) + return gophermap.ExtendMiddleware(fsroot, urlroot, settings)(handler) } // GopherDirectoryDefault serves up default files for directory path requests. @@ -69,61 +24,12 @@ func GopherFileHandler(fsroot, urlroot string, settings *gophermap.FileSystemSet // // It returns nil for any paths which don't correspond to a directory. func GopherDirectoryDefault(fsroot, urlroot string, settings *gophermap.FileSystemSettings) sr.Handler { - fsroot = strings.TrimRight(fsroot, "/") - - return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { - if !strings.HasPrefix(request.Path, urlroot) { - return nil - } - requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/") - - path := filepath.Join(fsroot, requestpath) - if isPrivate(path) { - return nil - } - if isd, err := isDir(path); err != nil { - return gopher.Error(err).Response() - } else if !isd { - return nil - } - - if settings == nil { - settings = &gophermap.FileSystemSettings{} - } - - for _, fname := range settings.DirMaps { - fpath := filepath.Join(path, fname) - if isf, err := isFile(fpath); err != nil { - return gopher.Error(err).Response() - } else if !isf { - continue - } - - file, err := os.Open(fpath) - if err != nil { - return gopher.Error(err).Response() - } - - if settings.ParseExtended { - defer func() { _ = file.Close() }() - - edoc, err := gophermap.ParseExtended(file, request.URL) - if err != nil { - return gopher.Error(err).Response() - } - - doc, _, err := edoc.Compatible(path, *settings) - if err != nil { - return gopher.Error(err).Response() - } - return doc.Response() - } else { - return gopher.File(gopher.MenuType, file) - } - } + if settings == nil { + return sr.HandlerFunc(func(_ context.Context, _ *sr.Request) *sr.Response { return nil }) + } - return nil - }) + handler := directoryDefault(gopher.ServerProtocol, fsroot, urlroot, false, settings.DirMaps...) + return gophermap.ExtendMiddleware(fsroot, urlroot, settings)(handler) } // GopherDirectoryListing produces a listing of the contents of any requested directories. @@ -136,13 +42,13 @@ func GopherDirectoryListing(fsroot, urlroot string, settings *gophermap.FileSyst if !strings.HasPrefix(request.Path, urlroot) { return nil } - requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/") - path := filepath.Join(fsroot, requestpath) - if isPrivate(path) { + dpath, _ := rebasePath(fsroot, urlroot, request) + + if isPrivate(dpath) { return nil } - if isd, err := isDir(path); err != nil { + if isd, err := isDir(dpath); err != nil { return gopher.Error(err).Response() } else if !isd { return nil @@ -151,7 +57,7 @@ func GopherDirectoryListing(fsroot, urlroot string, settings *gophermap.FileSyst if settings == nil { settings = &gophermap.FileSystemSettings{} } - doc, err := gophermap.ListDir(path, request.URL, *settings) + doc, err := gophermap.ListDir(dpath, request.URL, *settings) if err != nil { return gopher.Error(err).Response() } @@ -159,35 +65,3 @@ func GopherDirectoryListing(fsroot, urlroot string, settings *gophermap.FileSyst return doc.Response() }) } - -func isDir(path string) (bool, error) { - info, err := os.Stat(path) - if err != nil { - if isNotFound(err) { - err = nil - } - return false, err - } - return info.IsDir() && info.Mode()&4 == 4, nil -} - -func isFile(path string) (bool, error) { - info, err := os.Stat(path) - if err != nil { - if isNotFound(err) { - err = nil - } - return false, err - } - m := info.Mode() - - return m.IsRegular() && m&4 == 4, nil -} - -func isMap(path string, settings gophermap.FileSystemSettings) bool { - base := filepath.Base(path) - if base == "gophermap" || strings.HasSuffix(base, ".gph") || strings.HasSuffix(base, ".gophermap") { - return true - } - return slices.Contains(settings.DirMaps, filepath.Base(path)) -} diff --git a/contrib/fs/handlers.go b/contrib/fs/handlers.go new file mode 100644 index 0000000..75422d9 --- /dev/null +++ b/contrib/fs/handlers.go @@ -0,0 +1,162 @@ +package fs + +import ( + "context" + "net/url" + "os" + "path/filepath" + "strings" + "text/template" + + sr "tildegit.org/tjp/sliderule" +) + +func fileHandler(protocol sr.ServerProtocol, fsroot, urlroot string) sr.Handler { + fsroot = strings.TrimRight(fsroot, "/") + + return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { + if !strings.HasPrefix(request.Path, urlroot) { + return nil + } + + fpath, _ := rebasePath(fsroot, urlroot, request) + + if isPrivate(fpath) { + return nil + } + if isf, err := isFile(fpath); err != nil { + return protocol.TemporaryServerError(err) + } else if !isf { + return nil + } + + file, err := os.Open(fpath) + if err != nil { + return protocol.TemporaryServerError(err) + } + return protocol.Success(filepath.Base(fpath), file) + }) +} + +func directoryDefault( + protocol sr.ServerProtocol, + fsroot string, + urlroot string, + redirectSlash bool, + filenames ...string, +) sr.Handler { + fsroot = strings.TrimRight(fsroot, "/") + + return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { + if !strings.HasPrefix(request.Path, urlroot) { + return nil + } + + fpath, _ := rebasePath(fsroot, urlroot, request) + + if isPrivate(fpath) { + return nil + } + if isd, err := isDir(fpath); err != nil { + return protocol.TemporaryServerError(err) + } else if !isd { + return nil + } + + if redirectSlash && !strings.HasSuffix(request.Path, "/") { + return protocol.PermanentRedirect(appendSlash(request.URL)) + } + + for _, fname := range filenames { + fpath := filepath.Join(fpath, fname) + if isf, err := isFile(fpath); err != nil { + return protocol.TemporaryServerError(err) + } else if !isf { + continue + } + + file, err := os.Open(fpath) + if err != nil { + return protocol.TemporaryServerError(err) + } + return protocol.Success(filepath.Base(fpath), file) + } + + return nil + }) +} + +func directoryListing( + protocol sr.ServerProtocol, + fsroot string, + urlroot string, + successFilename string, + redirectSlash bool, + tmpl *template.Template, +) sr.Handler { + fsroot = strings.TrimRight(fsroot, "/") + + return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { + if !strings.HasPrefix(request.Path, urlroot) { + return nil + } + + dpath, rpath := rebasePath(fsroot, urlroot, request) + + if isPrivate(dpath) { + return nil + } + if isd, err := isDir(dpath); err != nil { + return protocol.TemporaryServerError(err) + } else if !isd { + return nil + } + + if redirectSlash && !strings.HasSuffix(request.Path, "/") { + return protocol.PermanentRedirect(appendSlash(request.URL)) + } + + body, err := RenderDirectoryListing(dpath, rpath, tmpl, request.Server) + if err != nil { + return protocol.TemporaryServerError(err) + } + + return protocol.Success(successFilename, body) + }) +} + +func rebasePath(fsroot, urlroot string, request *sr.Request) (string, string) { + p := strings.TrimPrefix(request.Path, urlroot) + p = strings.Trim(p, "/") + return filepath.Join(fsroot, p), p +} + +func appendSlash(u *url.URL) *url.URL { + v := *u + v.Path += "/" + return &v +} + +func isDir(path string) (bool, error) { + info, err := os.Stat(path) + if err != nil { + if isNotFound(err) { + err = nil + } + return false, err + } + return info.IsDir() && info.Mode()&4 == 4, nil +} + +func isFile(path string) (bool, error) { + info, err := os.Stat(path) + if err != nil { + if isNotFound(err) { + err = nil + } + return false, err + } + m := info.Mode() + + return m.IsRegular() && m&4 == 4, nil +} diff --git a/contrib/fs/spartan.go b/contrib/fs/spartan.go index bee274a..d97edd1 100644 --- a/contrib/fs/spartan.go +++ b/contrib/fs/spartan.go @@ -1,10 +1,6 @@ package fs import ( - "context" - "os" - "path/filepath" - "strings" "text/template" sr "tildegit.org/tjp/sliderule" @@ -15,30 +11,7 @@ import ( // // It only serves responses for paths which correspond to regular files or symlinks to them. func SpartanFileHandler(fsroot, urlroot string) sr.Handler { - fsroot = strings.TrimRight(fsroot, "/") - - return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { - if !strings.HasPrefix(request.Path, urlroot) { - return nil - } - requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/") - - fpath := filepath.Join(fsroot, requestpath) - if isPrivate(fpath) { - return nil - } - if isf, err := isFile(fpath); err != nil { - return spartan.ServerError(err) - } else if !isf { - return nil - } - - file, err := os.Open(fpath) - if err != nil { - return spartan.ServerError(err) - } - return spartan.Success(mediaType(fpath), file) - }) + return fileHandler(spartan.ServerProtocol, fsroot, urlroot) } // SpartanDirectoryDefault serves up default files for directory path requests. @@ -52,47 +25,7 @@ func SpartanFileHandler(fsroot, urlroot string) sr.Handler { // redirects to the URL with the slash appended. This is necessary for relative links // in the directory's contents to function properly. func SpartanDirectoryDefault(fsroot, urlroot string, filenames ...string) sr.Handler { - fsroot = strings.TrimRight(fsroot, "/") - - return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { - if !strings.HasPrefix(request.Path, urlroot) { - return nil - } - - if !strings.HasSuffix(request.Path, "/") { - u := *request.URL - u.Path += "/" - return spartan.Redirect(u.String()) - } - - requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/") - fpath := filepath.Join(fsroot, requestpath) - if isPrivate(fpath) { - return nil - } - if isd, err := isDir(fpath); err != nil { - return spartan.ServerError(err) - } else if !isd { - return nil - } - - for _, fname := range filenames { - candidatepath := filepath.Join(fpath, fname) - if isf, err := isFile(candidatepath); err != nil { - return spartan.ServerError(err) - } else if !isf { - continue - } - - file, err := os.Open(candidatepath) - if err != nil { - return spartan.ServerError(err) - } - return spartan.Success(mediaType(candidatepath), file) - } - - return nil - }) + return directoryDefault(spartan.ServerProtocol, fsroot, urlroot, true, filenames...) } // SpartanDirectoryListing produces a listing of the contents of any requested directories. @@ -105,40 +38,11 @@ func SpartanDirectoryDefault(fsroot, urlroot string, filenames ...string) sr.Han // // The template may be nil, in which case DefaultSpartanDirectoryList is used instead. The // template is then processed with RenderDirectoryListing. -func SpartanDirectoryListing(fsroot, urlroot string, template *template.Template) sr.Handler { - fsroot = strings.TrimRight(fsroot, "/") - - return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { - if !strings.HasSuffix(request.Path, "/") { - u := *request.URL - u.Path += "/" - return spartan.Redirect(u.String()) - } - if !strings.HasPrefix(request.Path, urlroot) { - return nil - } - requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/") - - fpath := filepath.Join(fsroot, requestpath) - if isPrivate(fpath) { - return nil - } - if isd, err := isDir(fpath); err != nil { - return spartan.ServerError(err) - } else if !isd { - return nil - } - - if template == nil { - template = DefaultSpartanDirectoryList - } - body, err := RenderDirectoryListing(fpath, requestpath, template, request.Server) - if err != nil { - return spartan.ServerError(err) - } - - return spartan.Success("text/gemini", body) - }) +func SpartanDirectoryListing(fsroot, urlroot string, tmpl *template.Template) sr.Handler { + if tmpl == nil { + tmpl = DefaultSpartanDirectoryList + } + return directoryListing(spartan.ServerProtocol, fsroot, urlroot, "file.gmi", true, tmpl) } // DefaultSpartanDirectoryList is a tmeplate which renders a reasonable gemtext dir listing. |