Merge branch 'feature/static' into develop

This commit is contained in:
Maxim Lebedev 2023-11-09 06:57:27 +06:00
commit 6012945d06
Signed by: toby3d
GPG Key ID: 1F14E25B7C119FC5
15 changed files with 271 additions and 23 deletions

44
internal/domain/file.go Normal file
View File

@ -0,0 +1,44 @@
package domain
import (
"mime"
"path/filepath"
"strings"
"time"
)
type File struct {
Path string
Updated time.Time
Content []byte
}
// LogicalName returns full file name without directory path.
func (f File) LogicalName() string {
return filepath.Base(f.Path)
}
// BaseFileName returns file name without extention and directory path.
func (f File) BaseFileName() string {
base := filepath.Base(f.Path)
return strings.TrimSuffix(base, filepath.Ext(base))
}
// Ext returns file extention.
func (f File) Ext() string {
if ext := filepath.Ext(f.Path); len(ext) > 1 {
return ext[1:]
}
return ""
}
// Dir returns file directory.
func (f File) Dir() string {
return filepath.Dir(f.Path)
}
func (f File) MediaType() string {
return mime.TypeByExtension(f.Ext())
}

4
internal/domain/files.go Normal file
View File

@ -0,0 +1,4 @@
package domain
// TODO(toby3d): search by glob pattern, type or name/id.
type Files []*File

View File

@ -7,4 +7,5 @@ type Page struct {
Params map[string]any Params map[string]any
Title string Title string
Content []byte Content []byte
Files Files
} }

View File

@ -11,4 +11,5 @@ type Site struct {
TimeZone *time.Location TimeZone *time.Location
Params map[string]any Params map[string]any
Title string Title string
Files Files
} }

View File

@ -36,17 +36,17 @@ func NewFileSystemPageRepository(rootDir fs.FS) page.Repository {
} }
} }
func (repo *fileSystemPageRepository) Get(ctx context.Context, lang language.Tag, path string) (*domain.Page, error) { func (repo *fileSystemPageRepository) Get(ctx context.Context, lang language.Tag, p string) (*domain.Page, error) {
ext := ".md" ext := ".md"
if lang != language.Und { if lang != language.Und {
ext = "." + lang.String() + ext ext = "." + lang.String() + ext
} }
target := path + ext index := p + ext
f, err := repo.dir.Open(target) f, err := repo.dir.Open(index)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot open '%s' page file: %w", target, err) return nil, fmt.Errorf("cannot open '%s' page file: %w", index, err)
} }
defer f.Close() defer f.Close()
@ -60,5 +60,6 @@ func (repo *fileSystemPageRepository) Get(ctx context.Context, lang language.Tag
Title: data.Title, Title: data.Title,
Content: data.Content, Content: data.Content,
Params: data.Params, Params: data.Params,
Files: make([]*domain.File, 0),
}, nil }, nil
} }

View File

@ -33,23 +33,23 @@ func TestGet(t *testing.T) {
}{ }{
"index": { "index": {
input: path.Join("index"), input: path.Join("index"),
expect: &domain.Page{Content: []byte("index.md")}, expect: &domain.Page{Content: []byte("index.md"), Files: make([]*domain.File, 0)},
}, },
"file": { "file": {
input: path.Join("file"), input: path.Join("file"),
expect: &domain.Page{Content: []byte("file.md")}, expect: &domain.Page{Content: []byte("file.md"), Files: make([]*domain.File, 0)},
}, },
"folder": { "folder": {
input: path.Join("folder", "index"), input: path.Join("folder", "index"),
expect: &domain.Page{Content: []byte("folder/index.md")}, expect: &domain.Page{Content: []byte("folder/index.md"), Files: make([]*domain.File, 0)},
}, },
"both-file": { "both-file": {
input: path.Join("both"), input: path.Join("both"),
expect: &domain.Page{Content: []byte("both.md")}, expect: &domain.Page{Content: []byte("both.md"), Files: make([]*domain.File, 0)},
}, },
"both-folder": { "both-folder": {
input: path.Join("both", "index"), input: path.Join("both", "index"),
expect: &domain.Page{Content: []byte("both/index.md")}, expect: &domain.Page{Content: []byte("both/index.md"), Files: make([]*domain.File, 0)},
}, },
} { } {
name, tc := name, tc name, tc := name, tc

View File

@ -4,21 +4,25 @@ import (
"context" "context"
"fmt" "fmt"
"path" "path"
"slices"
"golang.org/x/text/language" "golang.org/x/text/language"
"source.toby3d.me/toby3d/home/internal/domain" "source.toby3d.me/toby3d/home/internal/domain"
"source.toby3d.me/toby3d/home/internal/page" "source.toby3d.me/toby3d/home/internal/page"
"source.toby3d.me/toby3d/home/internal/static"
"source.toby3d.me/toby3d/home/internal/urlutil" "source.toby3d.me/toby3d/home/internal/urlutil"
) )
type pageUseCase struct { type pageUseCase struct {
pages page.Repository pages page.Repository
statics static.Repository
} }
func NewPageUseCase(pages page.Repository) page.UseCase { func NewPageUseCase(pages page.Repository, statics static.Repository) page.UseCase {
return &pageUseCase{ return &pageUseCase{
pages: pages, pages: pages,
statics: statics,
} }
} }
@ -46,6 +50,18 @@ func (ucase *pageUseCase) Do(ctx context.Context, lang language.Tag, p string) (
continue continue
} }
if out.Files, _, err = ucase.statics.Fetch(ctx, path.Dir(targets[i])); err != nil {
return out, nil
}
for j := range out.Files {
if ext := out.Files[j].Ext(); ext != ".html" && ext != ".md" {
continue
}
out.Files = slices.Delete(out.Files, j, j+1)
}
return out, nil return out, nil
} }

View File

@ -11,6 +11,7 @@ import (
pagefsrepo "source.toby3d.me/toby3d/home/internal/page/repository/fs" pagefsrepo "source.toby3d.me/toby3d/home/internal/page/repository/fs"
"source.toby3d.me/toby3d/home/internal/page/usecase" "source.toby3d.me/toby3d/home/internal/page/usecase"
"source.toby3d.me/toby3d/home/internal/static"
) )
func TestDo(t *testing.T) { func TestDo(t *testing.T) {
@ -24,7 +25,7 @@ func TestDo(t *testing.T) {
filepath.Join("index.md"): &fstest.MapFile{Data: []byte(`index.md`)}, filepath.Join("index.md"): &fstest.MapFile{Data: []byte(`index.md`)},
}) })
ucase := usecase.NewPageUseCase(pages) ucase := usecase.NewPageUseCase(pages, static.NewDummyRepository())
for name, tc := range map[string]struct { for name, tc := range map[string]struct {
input string input string

View File

@ -35,6 +35,7 @@ func TestGet(t *testing.T) {
expect: &domain.Site{ expect: &domain.Site{
Language: language.English, Language: language.English,
Title: "example", Title: "example",
Params: make(map[string]any),
}, },
}, },
"russian": { "russian": {
@ -42,6 +43,7 @@ func TestGet(t *testing.T) {
expect: &domain.Site{ expect: &domain.Site{
Language: language.Russian, Language: language.Russian,
Title: "пример", Title: "пример",
Params: make(map[string]any),
}, },
}, },
} { } {

View File

@ -8,15 +8,18 @@ import (
"source.toby3d.me/toby3d/home/internal/domain" "source.toby3d.me/toby3d/home/internal/domain"
"source.toby3d.me/toby3d/home/internal/site" "source.toby3d.me/toby3d/home/internal/site"
"source.toby3d.me/toby3d/home/internal/static"
) )
type siteUseCase struct { type siteUseCase struct {
sites site.Repository sites site.Repository
statics static.Repository
} }
func NewSiteUseCase(sites site.Repository) site.UseCase { func NewSiteUseCase(sites site.Repository, statics static.Repository) site.UseCase {
return &siteUseCase{ return &siteUseCase{
sites: sites, sites: sites,
statics: statics,
} }
} }
@ -26,6 +29,8 @@ func (ucase *siteUseCase) Do(ctx context.Context, lang language.Tag) (*domain.Si
return nil, fmt.Errorf("cannot find base site data: %w", err) return nil, fmt.Errorf("cannot find base site data: %w", err)
} }
out.Files, _, _ = ucase.statics.Fetch(ctx, ".")
sub, err := ucase.sites.Get(ctx, lang) sub, err := ucase.sites.Get(ctx, lang)
if err != nil { if err != nil {
return out, nil return out, nil

View File

@ -0,0 +1,26 @@
package static
import (
"context"
"source.toby3d.me/toby3d/home/internal/domain"
)
type (
Repository interface {
Get(ctx context.Context, path string) (*domain.File, error)
Fetch(ctx context.Context, dir string) (domain.Files, int, error)
}
dummyRepository struct{}
)
func NewDummyRepository() dummyRepository {
return dummyRepository{}
}
func (dummyRepository) Get(ctx context.Context, path string) (*domain.File, error) { return nil, nil }
func (dummyRepository) Fetch(ctx context.Context, dir string) (domain.Files, int, error) {
return nil, 0, nil
}

View File

@ -0,0 +1,72 @@
package fs
import (
"context"
"fmt"
"io/fs"
"path/filepath"
"source.toby3d.me/toby3d/home/internal/domain"
"source.toby3d.me/toby3d/home/internal/static"
)
type fileServerStaticRepository struct {
root fs.FS
}
func NewFileServerStaticRepository(root fs.FS) static.Repository {
return &fileServerStaticRepository{
root: root,
}
}
func (repo *fileServerStaticRepository) Get(ctx context.Context, p string) (*domain.File, error) {
info, err := fs.Stat(repo.root, p)
if err != nil {
return nil, fmt.Errorf("cannot stat static on path '%s': %w", p, err)
}
content, err := fs.ReadFile(repo.root, p)
if err != nil {
return nil, fmt.Errorf("cannot read static content on path '%s': %w", p, err)
}
return &domain.File{
Path: p,
Updated: info.ModTime(),
Content: content,
}, nil
}
func (repo *fileServerStaticRepository) Fetch(ctx context.Context, d string) (domain.Files, int, error) {
entries, err := fs.ReadDir(repo.root, d)
if err != nil {
return nil, 0, fmt.Errorf("cannot read directory on path '%s': %w", d, err)
}
out := make(domain.Files, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() {
continue
}
content, err := fs.ReadFile(repo.root, filepath.Join(d, entry.Name()))
if err != nil {
continue
}
info, err := entry.Info()
if err != nil {
continue
}
out = append(out, &domain.File{
Path: filepath.Join(d, info.Name()),
Updated: info.ModTime(),
Content: content,
})
}
return out, len(out), nil
}

View File

@ -0,0 +1,11 @@
package static
import (
"context"
"source.toby3d.me/toby3d/home/internal/domain"
)
type UseCase interface {
Do(ctx context.Context, path string) (*domain.File, error)
}

View File

@ -0,0 +1,37 @@
package usecase
import (
"context"
"fmt"
"io/fs"
"path"
"strings"
"source.toby3d.me/toby3d/home/internal/domain"
"source.toby3d.me/toby3d/home/internal/static"
)
type staticUseCase struct {
statics static.Repository
}
func NewStaticUseCase(statics static.Repository) static.UseCase {
return &staticUseCase{
statics: statics,
}
}
func (ucase *staticUseCase) Do(ctx context.Context, p string) (*domain.File, error) {
p = strings.TrimPrefix(path.Clean(p), "/")
if ext := path.Ext(p); ext == ".html" || ext == ".md" {
return nil, fs.ErrNotExist
}
f, err := ucase.statics.Get(ctx, p)
if err != nil {
return nil, fmt.Errorf("cannot get static file: %w", err)
}
return f, nil
}

41
main.go
View File

@ -5,9 +5,11 @@
package main package main
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"flag" "flag"
"io/fs"
"log" "log"
"net" "net"
"net/http" "net/http"
@ -25,10 +27,13 @@ import (
"source.toby3d.me/toby3d/home/internal/common" "source.toby3d.me/toby3d/home/internal/common"
"source.toby3d.me/toby3d/home/internal/domain" "source.toby3d.me/toby3d/home/internal/domain"
"source.toby3d.me/toby3d/home/internal/page"
pagefsrepo "source.toby3d.me/toby3d/home/internal/page/repository/fs" pagefsrepo "source.toby3d.me/toby3d/home/internal/page/repository/fs"
pageucase "source.toby3d.me/toby3d/home/internal/page/usecase" pageucase "source.toby3d.me/toby3d/home/internal/page/usecase"
sitefsrepo "source.toby3d.me/toby3d/home/internal/site/repository/fs" sitefsrepo "source.toby3d.me/toby3d/home/internal/site/repository/fs"
siteucase "source.toby3d.me/toby3d/home/internal/site/usecase" siteucase "source.toby3d.me/toby3d/home/internal/site/usecase"
staticfsrepo "source.toby3d.me/toby3d/home/internal/static/repository/fs"
staticucase "source.toby3d.me/toby3d/home/internal/static/usecase"
themefsrepo "source.toby3d.me/toby3d/home/internal/theme/repository/fs" themefsrepo "source.toby3d.me/toby3d/home/internal/theme/repository/fs"
themeucase "source.toby3d.me/toby3d/home/internal/theme/usecase" themeucase "source.toby3d.me/toby3d/home/internal/theme/usecase"
) )
@ -86,14 +91,17 @@ func main() {
contentDir := os.DirFS(config.ContentDir) contentDir := os.DirFS(config.ContentDir)
themeDir := os.DirFS(config.ThemeDir) themeDir := os.DirFS(config.ThemeDir)
statics := staticfsrepo.NewFileServerStaticRepository(contentDir)
staticer := staticucase.NewStaticUseCase(statics)
themes := themefsrepo.NewFileSystemThemeRepository(themeDir) themes := themefsrepo.NewFileSystemThemeRepository(themeDir)
themer := themeucase.NewThemeUseCase(themes) themer := themeucase.NewThemeUseCase(themes)
sites := sitefsrepo.NewFileSystemSiteRepository(contentDir) sites := sitefsrepo.NewFileSystemSiteRepository(contentDir)
siter := siteucase.NewSiteUseCase(sites) siter := siteucase.NewSiteUseCase(sites, statics)
pages := pagefsrepo.NewFileSystemPageRepository(contentDir) pages := pagefsrepo.NewFileSystemPageRepository(contentDir)
pager := pageucase.NewPageUseCase(pages) pager := pageucase.NewPageUseCase(pages, statics)
server := &http.Server{ server := &http.Server{
Addr: config.AddrPort().String(), Addr: config.AddrPort().String(),
@ -105,16 +113,35 @@ func main() {
lang, _, _ := matcher.Match(tags...) lang, _, _ := matcher.Match(tags...)
site, err := siter.Do(r.Context(), lang) s, err := siter.Do(r.Context(), lang)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
page, err := pager.Do(r.Context(), lang, r.URL.Path) p, err := pager.Do(r.Context(), lang, r.URL.Path)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) if !errors.Is(err, page.ErrNotExist) {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
f, err := staticer.Do(r.Context(), r.URL.Path)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.ServeContent(w, r, f.LogicalName(), f.Updated, bytes.NewReader(f.Content))
return return
} }
@ -128,8 +155,8 @@ func main() {
w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8) w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8)
if err = tpl.Execute(w, &Context{ if err = tpl.Execute(w, &Context{
Site: site, Site: s,
Page: page, Page: p,
}); err != nil { }); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
} }