Compare commits
11 Commits
555b8cc333
...
0656fc8ca4
Author | SHA1 | Date |
---|---|---|
Maxim Lebedev | 0656fc8ca4 | |
Maxim Lebedev | 41dc86eb62 | |
Maxim Lebedev | 393d7e0fad | |
Maxim Lebedev | 860d56cfc5 | |
Maxim Lebedev | 5c75e6f9e9 | |
Maxim Lebedev | b10c237f50 | |
Maxim Lebedev | 867782367e | |
Maxim Lebedev | 62bc90fd3e | |
Maxim Lebedev | 3a2515c255 | |
Maxim Lebedev | c8c5a89269 | |
Maxim Lebedev | 22422d0ad2 |
|
@ -3,15 +3,16 @@ package domain
|
|||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ContentDir string `env:"CONTENT_DIR" envDefault:"content/"`
|
||||
ContentDir string `env:"CONTENT_DIR" envDefault:"content"`
|
||||
Host string `env:"HOST" envDefault:"0.0.0.0"`
|
||||
ThemeDir string `env:"THEME_DIR" envDefault:"theme/"`
|
||||
StaticDir string `env:"STATIC_DIR" envDefault:"static/"`
|
||||
ThemeDir string `env:"THEME_DIR" envDefault:"theme"`
|
||||
StaticDir string `env:"STATIC_DIR" envDefault:"static"`
|
||||
Port uint16 `env:"PORT" envDefault:"3000"`
|
||||
}
|
||||
|
||||
|
@ -19,10 +20,10 @@ func TestConfig(tb testing.TB) *Config {
|
|||
tb.Helper()
|
||||
|
||||
return &Config{
|
||||
ContentDir: "testdata/content/",
|
||||
ContentDir: filepath.Join("testdata", "content"),
|
||||
Host: "0.0.0.0",
|
||||
ThemeDir: "testdata/theme/",
|
||||
StaticDir: "testdata/static/",
|
||||
ThemeDir: filepath.Join("testdata", "theme"),
|
||||
StaticDir: filepath.Join("testdata", "static"),
|
||||
Port: 3000,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ func NewFile(path string) File {
|
|||
Language: LanguageUnd,
|
||||
baseFileName: "",
|
||||
contentBaseName: "",
|
||||
dir: filepath.Dir(path) + "/",
|
||||
dir: filepath.Dir(path) + string(filepath.Separator),
|
||||
ext: strings.TrimPrefix(filepath.Ext(path), "."),
|
||||
filename: path,
|
||||
logicalName: filepath.Base(path),
|
||||
|
@ -32,7 +32,6 @@ func NewFile(path string) File {
|
|||
translationBaseName: "",
|
||||
uniqueId: "",
|
||||
}
|
||||
out.path, _ = filepath.Abs(path)
|
||||
out.baseFileName = strings.TrimSuffix(out.logicalName, filepath.Ext(out.logicalName))
|
||||
|
||||
parts := strings.Split(out.baseFileName, ".")
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
|
@ -19,7 +18,6 @@ type Resource struct {
|
|||
File File
|
||||
|
||||
modTime time.Time
|
||||
reader io.ReadSeeker
|
||||
params map[string]any // TODO(toby3d): set from Page configuration
|
||||
mediaType MediaType
|
||||
key string
|
||||
|
@ -29,7 +27,7 @@ type Resource struct {
|
|||
image image.Config
|
||||
}
|
||||
|
||||
func NewResource(modTime time.Time, content []byte, key string) *Resource {
|
||||
func NewResource(modTime time.Time, r io.Reader, key string) *Resource {
|
||||
mediaType, _, _ := mime.ParseMediaType(mime.TypeByExtension(path.Ext(key)))
|
||||
out := &Resource{
|
||||
File: NewFile(key),
|
||||
|
@ -39,12 +37,11 @@ func NewResource(modTime time.Time, content []byte, key string) *Resource {
|
|||
title: "", // TODO(toby3d): set from Page configuration
|
||||
params: make(map[string]any), // TODO(toby3d): set from Page configuration
|
||||
mediaType: NewMediaType(mediaType),
|
||||
reader: bytes.NewReader(content),
|
||||
}
|
||||
|
||||
switch path.Ext(key) {
|
||||
default:
|
||||
out.resourceType, _ = ParseResourceType(out.mediaType.mainType)
|
||||
out.resourceType = ResourceType(out.mediaType.mainType)
|
||||
case ".md":
|
||||
out.resourceType = ResourceTypePage
|
||||
case ".webmanifest":
|
||||
|
@ -53,7 +50,7 @@ func NewResource(modTime time.Time, content []byte, key string) *Resource {
|
|||
|
||||
switch out.resourceType {
|
||||
case ResourceTypeImage:
|
||||
out.image, _, _ = image.DecodeConfig(out.reader)
|
||||
out.image, _, _ = image.DecodeConfig(r)
|
||||
}
|
||||
|
||||
return out
|
||||
|
@ -85,20 +82,6 @@ func (r Resource) ResourceType() ResourceType {
|
|||
return r.resourceType
|
||||
}
|
||||
|
||||
func (r Resource) Content() []byte {
|
||||
content, _ := io.ReadAll(r.reader)
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
func (r Resource) Read(p []byte) (int, error) {
|
||||
return r.reader.Read(p)
|
||||
}
|
||||
|
||||
func (r Resource) Seek(offset int64, whence int) (int64, error) {
|
||||
return r.reader.Seek(offset, whence)
|
||||
}
|
||||
|
||||
func (f Resource) GoString() string {
|
||||
return "domain.Resource(" + f.key + ")"
|
||||
}
|
||||
|
|
|
@ -2,44 +2,24 @@ package domain
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ResourceType struct {
|
||||
resourceType string
|
||||
}
|
||||
type ResourceType string
|
||||
|
||||
var (
|
||||
ResourceTypeUnd ResourceType = ResourceType{} // "und"
|
||||
ResourceTypeAudio ResourceType = ResourceType{"audio"} // "audio"
|
||||
ResourceTypeImage ResourceType = ResourceType{"image"} // "image"
|
||||
ResourceTypePage ResourceType = ResourceType{"page"} // "page"
|
||||
ResourceTypeText ResourceType = ResourceType{"text"} // "text"
|
||||
ResourceTypeVideo ResourceType = ResourceType{"video"} // "video"
|
||||
ResourceTypeUnd ResourceType = "" // "und"
|
||||
ResourceTypeAudio ResourceType = "audio" // "audio"
|
||||
ResourceTypeImage ResourceType = "image" // "image"
|
||||
ResourceTypePage ResourceType = "page" // "page"
|
||||
ResourceTypeText ResourceType = "text" // "text"
|
||||
ResourceTypeVideo ResourceType = "video" // "video"
|
||||
)
|
||||
|
||||
var ErrResourceType error = errors.New("unsupported ResourceType enum")
|
||||
|
||||
var stringsResourceTypes = map[string]ResourceType{
|
||||
ResourceTypeAudio.resourceType: ResourceTypeAudio,
|
||||
ResourceTypeImage.resourceType: ResourceTypeImage,
|
||||
ResourceTypePage.resourceType: ResourceTypePage,
|
||||
ResourceTypeText.resourceType: ResourceTypeText,
|
||||
ResourceTypeVideo.resourceType: ResourceTypeVideo,
|
||||
}
|
||||
|
||||
func ParseResourceType(raw string) (ResourceType, error) {
|
||||
if rt, ok := stringsResourceTypes[strings.ToLower(raw)]; ok {
|
||||
return rt, nil
|
||||
}
|
||||
|
||||
return ResourceTypeUnd, fmt.Errorf("%w: got '%s', want %s", ErrResourceType, raw, stringsResourceTypes)
|
||||
}
|
||||
|
||||
func (rt ResourceType) String() string {
|
||||
if rt.resourceType != "" {
|
||||
return rt.resourceType
|
||||
if rt != "" {
|
||||
return string(rt)
|
||||
}
|
||||
|
||||
return "und"
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Static struct {
|
||||
modTime time.Time
|
||||
readSeeker io.ReadSeeker
|
||||
name string
|
||||
}
|
||||
|
||||
func NewStatic(rs io.ReadSeeker, modTime time.Time, name string) *Static {
|
||||
return &Static{
|
||||
name: name,
|
||||
modTime: modTime,
|
||||
readSeeker: rs,
|
||||
}
|
||||
}
|
||||
|
||||
func (s Static) Name() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
func (s Static) ModTime() time.Time {
|
||||
return s.modTime
|
||||
}
|
||||
|
||||
func (s *Static) Read(p []byte) (int, error) {
|
||||
return s.readSeeker.Read(p)
|
||||
}
|
||||
|
||||
func (s *Static) Seek(offset int64, whence int) (int64, error) {
|
||||
return s.readSeeker.Seek(offset, whence)
|
||||
}
|
|
@ -6,7 +6,6 @@ import (
|
|||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"io/fs"
|
||||
|
||||
_ "golang.org/x/image/bmp"
|
||||
|
@ -38,12 +37,7 @@ func (repo *fileServerResourceRepository) Get(ctx context.Context, p string) (*d
|
|||
}
|
||||
defer f.Close()
|
||||
|
||||
content, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read resource content on path '%s': %w", p, err)
|
||||
}
|
||||
|
||||
return domain.NewResource(info.ModTime(), content, p), nil
|
||||
return domain.NewResource(info.ModTime(), f, p), nil
|
||||
}
|
||||
|
||||
func (repo *fileServerResourceRepository) Fetch(ctx context.Context, pattern string) (domain.Resources, int, error) {
|
||||
|
|
|
@ -8,11 +8,8 @@ import (
|
|||
|
||||
type (
|
||||
Repository interface {
|
||||
// Get returns Resource on path if exists
|
||||
Get(ctx context.Context, path string) (*domain.Resource, error)
|
||||
|
||||
// Fetch returns all resources from dir recursevly.
|
||||
Fetch(ctx context.Context, pattern string) (domain.Resources, int, error)
|
||||
// Get returns Static on path if exists
|
||||
Get(ctx context.Context, path string) (*domain.Static, error)
|
||||
}
|
||||
|
||||
dummyRepository struct{}
|
||||
|
@ -22,10 +19,6 @@ func NewDummyRepository() dummyRepository {
|
|||
return dummyRepository{}
|
||||
}
|
||||
|
||||
func (dummyRepository) Get(ctx context.Context, path string) (*domain.Resource, error) {
|
||||
func (dummyRepository) Get(ctx context.Context, path string) (*domain.Static, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (dummyRepository) Fetch(ctx context.Context, pattern string) (domain.Resources, int, error) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package fs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
_ "image/gif"
|
||||
|
@ -26,7 +27,7 @@ func NewFileServerStaticRepository(root fs.FS) static.Repository {
|
|||
}
|
||||
}
|
||||
|
||||
func (repo *fileServerStaticRepository) Get(ctx context.Context, p string) (*domain.Resource, error) {
|
||||
func (repo *fileServerStaticRepository) Get(ctx context.Context, p string) (*domain.Static, error) {
|
||||
info, err := fs.Stat(repo.root, p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot stat static on path '%s': %w", p, err)
|
||||
|
@ -40,47 +41,8 @@ func (repo *fileServerStaticRepository) Get(ctx context.Context, p string) (*dom
|
|||
|
||||
content, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read static content on path '%s': %w", p, err)
|
||||
return nil, fmt.Errorf("cannot copy opened '%s' static contents into buffer: %w", p, err)
|
||||
}
|
||||
|
||||
return domain.NewResource(info.ModTime(), content, p), nil
|
||||
}
|
||||
|
||||
func (repo *fileServerStaticRepository) Fetch(ctx context.Context, pattern string) (domain.Resources, int, error) {
|
||||
var (
|
||||
err error
|
||||
matches []string
|
||||
)
|
||||
|
||||
if pattern != "" {
|
||||
if matches, err = fs.Glob(repo.root, pattern); err != nil {
|
||||
return nil, 0, fmt.Errorf("cannot match any static by pattern '%s': %w", pattern, err)
|
||||
}
|
||||
} else {
|
||||
if err = fs.WalkDir(repo.root, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("catched error while walk: %w", err)
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
matches = append(matches, path)
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, 0, fmt.Errorf("cannot walk through static directories: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
out := make(domain.Resources, 0, len(matches))
|
||||
|
||||
for i := range matches {
|
||||
if r, err := repo.Get(ctx, matches[i]); err == nil {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
|
||||
return out, len(out), nil
|
||||
return domain.NewStatic(bytes.NewReader(content), info.ModTime(), info.Name()), nil
|
||||
}
|
||||
|
|
|
@ -7,5 +7,5 @@ import (
|
|||
)
|
||||
|
||||
type UseCase interface {
|
||||
Do(ctx context.Context, path string) (*domain.Resource, error)
|
||||
Do(ctx context.Context, path string) (*domain.Static, error)
|
||||
}
|
||||
|
|
|
@ -20,13 +20,13 @@ func NewStaticUseCase(statics static.Repository) static.UseCase {
|
|||
}
|
||||
}
|
||||
|
||||
func (ucase *staticUseCase) Do(ctx context.Context, p string) (*domain.Resource, error) {
|
||||
func (ucase *staticUseCase) Do(ctx context.Context, p string) (*domain.Static, error) {
|
||||
p = strings.TrimPrefix(path.Clean(p), "/")
|
||||
|
||||
f, err := ucase.statics.Get(ctx, p)
|
||||
s, err := ucase.statics.Get(ctx, p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get static file: %w", err)
|
||||
}
|
||||
|
||||
return f, nil
|
||||
return s, nil
|
||||
}
|
||||
|
|
27
main.go
27
main.go
|
@ -5,10 +5,12 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
|
@ -83,13 +85,12 @@ func NewApp(ctx context.Context, config *domain.Config) (*App, error) {
|
|||
pages := pagefsrepo.NewFileSystemPageRepository(contentDir)
|
||||
pager := pageucase.NewPageUseCase(pages, resources)
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO(toby3d): use exists static use case or split that on static and resource modules?
|
||||
// INFO(toby3d): any static file is public and unprotected by design, so it's safe to search it
|
||||
// first before deep down to any page or it's resource which might be secured by middleware or
|
||||
// something else.
|
||||
static, err := staticer.Do(r.Context(), strings.TrimPrefix(r.URL.Path, "/"))
|
||||
if err == nil {
|
||||
http.ServeContent(w, r, static.Name(), domain.ResourceModTime(static), static)
|
||||
http.ServeContent(w, r, static.Name(), static.ModTime(), static)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -114,8 +115,7 @@ func NewApp(ctx context.Context, config *domain.Config) (*App, error) {
|
|||
|
||||
if s.DefaultLanguage != domain.LanguageUnd {
|
||||
supported = append(
|
||||
[]language.Tag{language.Make(s.DefaultLanguage.Code())},
|
||||
supported...,
|
||||
[]language.Tag{language.Make(s.DefaultLanguage.Code())}, supported...,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -164,7 +164,24 @@ func NewApp(ctx context.Context, config *domain.Config) (*App, error) {
|
|||
return
|
||||
}
|
||||
|
||||
http.ServeContent(w, r, res.Name(), domain.ResourceModTime(res), res)
|
||||
// TODO(toby3d) : ugly workaround, must be refactored
|
||||
resFile, err := contentDir.Open(res.File.Path())
|
||||
if err != nil {
|
||||
http.Error(w, "cannot open: "+err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
defer resFile.Close()
|
||||
|
||||
resBytes, err := io.ReadAll(resFile)
|
||||
if err != nil {
|
||||
http.Error(w, "cannot read all: "+err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
defer resFile.Close()
|
||||
|
||||
http.ServeContent(w, r, res.Name(), domain.ResourceModTime(res), bytes.NewReader(resBytes))
|
||||
|
||||
return
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue