Compare commits

...

11 Commits

11 changed files with 90 additions and 125 deletions

View File

@ -3,15 +3,16 @@ package domain
import ( import (
"net" "net"
"net/netip" "net/netip"
"path/filepath"
"strconv" "strconv"
"testing" "testing"
) )
type Config struct { 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"` Host string `env:"HOST" envDefault:"0.0.0.0"`
ThemeDir string `env:"THEME_DIR" envDefault:"theme/"` ThemeDir string `env:"THEME_DIR" envDefault:"theme"`
StaticDir string `env:"STATIC_DIR" envDefault:"static/"` StaticDir string `env:"STATIC_DIR" envDefault:"static"`
Port uint16 `env:"PORT" envDefault:"3000"` Port uint16 `env:"PORT" envDefault:"3000"`
} }
@ -19,10 +20,10 @@ func TestConfig(tb testing.TB) *Config {
tb.Helper() tb.Helper()
return &Config{ return &Config{
ContentDir: "testdata/content/", ContentDir: filepath.Join("testdata", "content"),
Host: "0.0.0.0", Host: "0.0.0.0",
ThemeDir: "testdata/theme/", ThemeDir: filepath.Join("testdata", "theme"),
StaticDir: "testdata/static/", StaticDir: filepath.Join("testdata", "static"),
Port: 3000, Port: 3000,
} }
} }

View File

@ -24,7 +24,7 @@ func NewFile(path string) File {
Language: LanguageUnd, Language: LanguageUnd,
baseFileName: "", baseFileName: "",
contentBaseName: "", contentBaseName: "",
dir: filepath.Dir(path) + "/", dir: filepath.Dir(path) + string(filepath.Separator),
ext: strings.TrimPrefix(filepath.Ext(path), "."), ext: strings.TrimPrefix(filepath.Ext(path), "."),
filename: path, filename: path,
logicalName: filepath.Base(path), logicalName: filepath.Base(path),
@ -32,7 +32,6 @@ func NewFile(path string) File {
translationBaseName: "", translationBaseName: "",
uniqueId: "", uniqueId: "",
} }
out.path, _ = filepath.Abs(path)
out.baseFileName = strings.TrimSuffix(out.logicalName, filepath.Ext(out.logicalName)) out.baseFileName = strings.TrimSuffix(out.logicalName, filepath.Ext(out.logicalName))
parts := strings.Split(out.baseFileName, ".") parts := strings.Split(out.baseFileName, ".")

View File

@ -1,7 +1,6 @@
package domain package domain
import ( import (
"bytes"
"image" "image"
_ "image/gif" _ "image/gif"
_ "image/jpeg" _ "image/jpeg"
@ -19,7 +18,6 @@ type Resource struct {
File File File File
modTime time.Time modTime time.Time
reader io.ReadSeeker
params map[string]any // TODO(toby3d): set from Page configuration params map[string]any // TODO(toby3d): set from Page configuration
mediaType MediaType mediaType MediaType
key string key string
@ -29,7 +27,7 @@ type Resource struct {
image image.Config 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))) mediaType, _, _ := mime.ParseMediaType(mime.TypeByExtension(path.Ext(key)))
out := &Resource{ out := &Resource{
File: NewFile(key), File: NewFile(key),
@ -39,12 +37,11 @@ func NewResource(modTime time.Time, content []byte, key string) *Resource {
title: "", // TODO(toby3d): set from Page configuration title: "", // TODO(toby3d): set from Page configuration
params: make(map[string]any), // TODO(toby3d): set from Page configuration params: make(map[string]any), // TODO(toby3d): set from Page configuration
mediaType: NewMediaType(mediaType), mediaType: NewMediaType(mediaType),
reader: bytes.NewReader(content),
} }
switch path.Ext(key) { switch path.Ext(key) {
default: default:
out.resourceType, _ = ParseResourceType(out.mediaType.mainType) out.resourceType = ResourceType(out.mediaType.mainType)
case ".md": case ".md":
out.resourceType = ResourceTypePage out.resourceType = ResourceTypePage
case ".webmanifest": case ".webmanifest":
@ -53,7 +50,7 @@ func NewResource(modTime time.Time, content []byte, key string) *Resource {
switch out.resourceType { switch out.resourceType {
case ResourceTypeImage: case ResourceTypeImage:
out.image, _, _ = image.DecodeConfig(out.reader) out.image, _, _ = image.DecodeConfig(r)
} }
return out return out
@ -85,20 +82,6 @@ func (r Resource) ResourceType() ResourceType {
return r.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 { func (f Resource) GoString() string {
return "domain.Resource(" + f.key + ")" return "domain.Resource(" + f.key + ")"
} }

View File

@ -2,44 +2,24 @@ package domain
import ( import (
"errors" "errors"
"fmt"
"strings"
) )
type ResourceType struct { type ResourceType string
resourceType string
}
var ( var (
ResourceTypeUnd ResourceType = ResourceType{} // "und" ResourceTypeUnd ResourceType = "" // "und"
ResourceTypeAudio ResourceType = ResourceType{"audio"} // "audio" ResourceTypeAudio ResourceType = "audio" // "audio"
ResourceTypeImage ResourceType = ResourceType{"image"} // "image" ResourceTypeImage ResourceType = "image" // "image"
ResourceTypePage ResourceType = ResourceType{"page"} // "page" ResourceTypePage ResourceType = "page" // "page"
ResourceTypeText ResourceType = ResourceType{"text"} // "text" ResourceTypeText ResourceType = "text" // "text"
ResourceTypeVideo ResourceType = ResourceType{"video"} // "video" ResourceTypeVideo ResourceType = "video" // "video"
) )
var ErrResourceType error = errors.New("unsupported ResourceType enum") 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 { func (rt ResourceType) String() string {
if rt.resourceType != "" { if rt != "" {
return rt.resourceType return string(rt)
} }
return "und" return "und"

36
internal/domain/static.go Normal file
View File

@ -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)
}

View File

@ -6,7 +6,6 @@ import (
_ "image/gif" _ "image/gif"
_ "image/jpeg" _ "image/jpeg"
_ "image/png" _ "image/png"
"io"
"io/fs" "io/fs"
_ "golang.org/x/image/bmp" _ "golang.org/x/image/bmp"
@ -38,12 +37,7 @@ func (repo *fileServerResourceRepository) Get(ctx context.Context, p string) (*d
} }
defer f.Close() defer f.Close()
content, err := io.ReadAll(f) return domain.NewResource(info.ModTime(), f, p), nil
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
} }
func (repo *fileServerResourceRepository) Fetch(ctx context.Context, pattern string) (domain.Resources, int, error) { func (repo *fileServerResourceRepository) Fetch(ctx context.Context, pattern string) (domain.Resources, int, error) {

View File

@ -8,11 +8,8 @@ import (
type ( type (
Repository interface { Repository interface {
// Get returns Resource on path if exists // Get returns Static on path if exists
Get(ctx context.Context, path string) (*domain.Resource, error) Get(ctx context.Context, path string) (*domain.Static, error)
// Fetch returns all resources from dir recursevly.
Fetch(ctx context.Context, pattern string) (domain.Resources, int, error)
} }
dummyRepository struct{} dummyRepository struct{}
@ -22,10 +19,6 @@ func NewDummyRepository() dummyRepository {
return 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 return nil, nil
} }
func (dummyRepository) Fetch(ctx context.Context, pattern string) (domain.Resources, int, error) {
return nil, 0, nil
}

View File

@ -1,6 +1,7 @@
package fs package fs
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
_ "image/gif" _ "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) info, err := fs.Stat(repo.root, p)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot stat static on path '%s': %w", p, err) 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) content, err := io.ReadAll(f)
if err != nil { 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 return domain.NewStatic(bytes.NewReader(content), info.ModTime(), info.Name()), 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
} }

View File

@ -7,5 +7,5 @@ import (
) )
type UseCase interface { type UseCase interface {
Do(ctx context.Context, path string) (*domain.Resource, error) Do(ctx context.Context, path string) (*domain.Static, error)
} }

View File

@ -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), "/") p = strings.TrimPrefix(path.Clean(p), "/")
f, err := ucase.statics.Get(ctx, p) s, err := ucase.statics.Get(ctx, p)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot get static file: %w", err) return nil, fmt.Errorf("cannot get static file: %w", err)
} }
return f, nil return s, nil
} }

27
main.go
View File

@ -5,10 +5,12 @@
package main package main
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"io"
"io/fs" "io/fs"
"log" "log"
"net" "net"
@ -83,13 +85,12 @@ func NewApp(ctx context.Context, config *domain.Config) (*App, error) {
pages := pagefsrepo.NewFileSystemPageRepository(contentDir) pages := pagefsrepo.NewFileSystemPageRepository(contentDir)
pager := pageucase.NewPageUseCase(pages, resources) pager := pageucase.NewPageUseCase(pages, resources)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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 // 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 // first before deep down to any page or it's resource which might be secured by middleware or
// something else. // something else.
static, err := staticer.Do(r.Context(), strings.TrimPrefix(r.URL.Path, "/")) static, err := staticer.Do(r.Context(), strings.TrimPrefix(r.URL.Path, "/"))
if err == nil { if err == nil {
http.ServeContent(w, r, static.Name(), domain.ResourceModTime(static), static) http.ServeContent(w, r, static.Name(), static.ModTime(), static)
return return
} }
@ -114,8 +115,7 @@ func NewApp(ctx context.Context, config *domain.Config) (*App, error) {
if s.DefaultLanguage != domain.LanguageUnd { if s.DefaultLanguage != domain.LanguageUnd {
supported = append( supported = append(
[]language.Tag{language.Make(s.DefaultLanguage.Code())}, []language.Tag{language.Make(s.DefaultLanguage.Code())}, supported...,
supported...,
) )
} }
@ -164,7 +164,24 @@ func NewApp(ctx context.Context, config *domain.Config) (*App, error) {
return 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 return
} }