Merge branch 'feature/static' into develop
This commit is contained in:
commit
6012945d06
|
@ -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())
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
// TODO(toby3d): search by glob pattern, type or name/id.
|
||||||
|
type Files []*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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
41
main.go
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue