Add grace period and fix bugs
This commit is contained in:
parent
e565c7be6d
commit
349e964364
14 changed files with 148 additions and 35 deletions
25
CHANGELOG.md
Normal file
25
CHANGELOG.md
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# Changelog
|
||||||
|
This file keeps track of changes in more human-readable fashion
|
||||||
|
|
||||||
|
# 5 May 2024
|
||||||
|
* Added this changelog
|
||||||
|
* Added grace period (as per suggestions)
|
||||||
|
* Set in config like `grace_period=3h26m` (via Go's `time.ParseDuration`)
|
||||||
|
* Defines how long the app should wait before switching to next day.
|
||||||
|
The example provided means you will still be editing yesterday's file at 3am, but at 3:27am it will be a new day
|
||||||
|
* When in effect, the header will show "(grace period active)" next to actual date
|
||||||
|
* Fun fact: if you suddenly increase the setting mid-day, you can have a "Tomorrow" in previous days list! :)
|
||||||
|
* This feature came with some free minor bug-fixes 'cause I had to re-check time management code.
|
||||||
|
Now let's hope it doesn't introduce any new ones! :D
|
||||||
|
* Began adding PWA manifest.json
|
||||||
|
* This will allow for (slightly) better mobile experience
|
||||||
|
* Still missing an icon, will be likely installable once I make one and test the app :D
|
||||||
|
* Known issue: making notes is impossible in the PWA, since you can't navigate to arbitrary page.
|
||||||
|
I might leave it as a WONTFIX or try to find a workaround
|
||||||
|
* Date is now shown in local language if possible (in case you add your own or use Russian)
|
||||||
|
* Bug fixes
|
||||||
|
* "Today" redirect from days list no longer uses UTC
|
||||||
|
* Date JS script no longer uses UTC
|
||||||
|
* The API no longer uses UTC for today
|
||||||
|
* `/public/` files is no longer behind auth
|
||||||
|
* Removed Sigmund® Corp. integration :)
|
22
README.md
22
README.md
|
@ -6,7 +6,7 @@ This project is *very* opinionated and minimal, and is designed primarily for my
|
||||||
As a result, I can't guarantee that it's either secure or stable.
|
As a result, I can't guarantee that it's either secure or stable.
|
||||||
|
|
||||||
## Features:
|
## Features:
|
||||||
* Each day, you get a text file. You have until 23:59 of that very day to finalise it.
|
* Each day, you get a new text file. You have until the end of that very day to finalise it.
|
||||||
* You can save named notes to document milestones, big events, or just a nice game you played this month
|
* You can save named notes to document milestones, big events, or just a nice game you played this month
|
||||||
* You can easily export entire `data` dir in a `.zip` archive for backups
|
* You can easily export entire `data` dir in a `.zip` archive for backups
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ data
|
||||||
config
|
config
|
||||||
+-- config.txt
|
+-- config.txt
|
||||||
```
|
```
|
||||||
|
Deleting notes is done by clearing contents and clicking "Save" - the app deletes empty files when saving.
|
||||||
|
|
||||||
### Config options:
|
### Config options:
|
||||||
Below are defaults of config.txt. The settings are defined in newline separated key=value pairs.
|
Below are defaults of config.txt. The settings are defined in newline separated key=value pairs.
|
||||||
|
@ -45,6 +46,7 @@ username=admin # Your username
|
||||||
password=admin # Your password
|
password=admin # Your password
|
||||||
port=7101 # What port to run on (probably leave on 7101 if using docker)
|
port=7101 # What port to run on (probably leave on 7101 if using docker)
|
||||||
timezone=Local # IANA Time zone database identifier ("UTC", Local", "Europe/Moscow" etc.), Defaults to Local if can't parse.
|
timezone=Local # IANA Time zone database identifier ("UTC", Local", "Europe/Moscow" etc.), Defaults to Local if can't parse.
|
||||||
|
grace_period=0s # Time after a new day begins, but before the switch to next day's file. e.g. 2h30m - files will change at 2:30am
|
||||||
language=en # ISO-639 language code (currently supported - en, ru)
|
language=en # ISO-639 language code (currently supported - en, ru)
|
||||||
log_to_file=false # Whether to write logs to a file
|
log_to_file=false # Whether to write logs to a file
|
||||||
log_file=config/log.txt # Where to store the log file if it is enabled
|
log_file=config/log.txt # Where to store the log file if it is enabled
|
||||||
|
@ -75,3 +77,21 @@ If you for some reason decide to run plain executable instead of docker, it supp
|
||||||
-debug
|
-debug
|
||||||
show debug log
|
show debug log
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### API methods
|
||||||
|
You can access the API at `/api/<method>`. They are protected by same HTTP Basic Auth as "normal" site.
|
||||||
|
```
|
||||||
|
GET /today - get file contents for today
|
||||||
|
POST /today - save request body into today's file
|
||||||
|
GET /day - get JSON list of all daily entries
|
||||||
|
GET /day/<name> - get file contents for a specific day
|
||||||
|
|
||||||
|
GET /notes - get JSON list of all named notes
|
||||||
|
GET /notes/<name> - get file contents for a specific note
|
||||||
|
POST /notes/<name> - save request body into a named note
|
||||||
|
|
||||||
|
GET /export - get .zip archive of entire data directory
|
||||||
|
|
||||||
|
GET /readme - get file contents for readme.txt in data dir's root
|
||||||
|
POST /readme - save request body into readme.txt
|
||||||
|
```
|
2
TODO.md
2
TODO.md
|
@ -2,7 +2,7 @@
|
||||||
List of things to add to this project
|
List of things to add to this project
|
||||||
|
|
||||||
* CI/CD pipeline
|
* CI/CD pipeline
|
||||||
* Better docs in case others want to use ths for some reason
|
* Better docs in case others want to use this for some reason
|
||||||
* PWA support? I heard it's like installing websites as apps, could be useful!
|
* PWA support? I heard it's like installing websites as apps, could be useful!
|
||||||
* Check export function for improvements
|
* Check export function for improvements
|
||||||
* *Go* dependency-less? <-- this is a terrible idea
|
* *Go* dependency-less? <-- this is a terrible idea
|
5
api.go
5
api.go
|
@ -8,7 +8,6 @@ import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandleWrite checks for error in ResponseWriter.Write output
|
// HandleWrite checks for error in ResponseWriter.Write output
|
||||||
|
@ -78,12 +77,12 @@ func GetDayApi(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// GetTodayApi runs GetFile with today's date as filename
|
// GetTodayApi runs GetFile with today's date as filename
|
||||||
func GetTodayApi(w http.ResponseWriter, _ *http.Request) {
|
func GetTodayApi(w http.ResponseWriter, _ *http.Request) {
|
||||||
GetFile("day/"+time.Now().Format(time.DateOnly), w)
|
GetFile("day/"+TodayDate(), w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostTodayApi runs PostFile with today's date as filename
|
// PostTodayApi runs PostFile with today's date as filename
|
||||||
func PostTodayApi(w http.ResponseWriter, r *http.Request) {
|
func PostTodayApi(w http.ResponseWriter, r *http.Request) {
|
||||||
PostFile("day/"+time.Now().Format(time.DateOnly), w, r)
|
PostFile("day/"+TodayDate(), w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNoteApi returns contents of a note specified in URL
|
// GetNoteApi returns contents of a note specified in URL
|
||||||
|
|
|
@ -20,6 +20,7 @@ type Config struct {
|
||||||
Password string `config:"password" type:"string"`
|
Password string `config:"password" type:"string"`
|
||||||
Port int `config:"port" type:"int"`
|
Port int `config:"port" type:"int"`
|
||||||
Timezone *time.Location `config:"timezone" type:"location"`
|
Timezone *time.Location `config:"timezone" type:"location"`
|
||||||
|
GraceTime time.Duration `config:"grace_period" type:"duration"`
|
||||||
Language string `config:"language" type:"string"`
|
Language string `config:"language" type:"string"`
|
||||||
LogToFile bool `config:"log_to_file" type:"bool"`
|
LogToFile bool `config:"log_to_file" type:"bool"`
|
||||||
LogFile string `config:"log_file" type:"string"`
|
LogFile string `config:"log_file" type:"string"`
|
||||||
|
@ -108,6 +109,13 @@ func (c *Config) Reload() error {
|
||||||
}
|
}
|
||||||
case "location":
|
case "location":
|
||||||
timezone = v
|
timezone = v
|
||||||
|
case "duration":
|
||||||
|
{
|
||||||
|
numVal, err := time.ParseDuration(v)
|
||||||
|
if err == nil {
|
||||||
|
fieldElem.SetInt(int64(numVal))
|
||||||
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
fieldElem.SetString(v)
|
fieldElem.SetString(v)
|
||||||
}
|
}
|
||||||
|
@ -134,6 +142,7 @@ func ConfigInit() Config {
|
||||||
Timezone: time.Local,
|
Timezone: time.Local,
|
||||||
Language: "en",
|
Language: "en",
|
||||||
LogFile: "config/log.txt",
|
LogFile: "config/log.txt",
|
||||||
|
GraceTime: 0,
|
||||||
}
|
}
|
||||||
err := cfg.Reload()
|
err := cfg.Reload()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -2,6 +2,7 @@ username=admin
|
||||||
password=admin
|
password=admin
|
||||||
port=7101
|
port=7101
|
||||||
timezone=Local
|
timezone=Local
|
||||||
|
grace_period=0s
|
||||||
language=en
|
language=en
|
||||||
log_to_file=false
|
log_to_file=false
|
||||||
log_file=config/log.txt
|
log_file=config/log.txt
|
||||||
|
|
21
files.go
21
files.go
|
@ -75,12 +75,29 @@ func ListFiles(directory string) ([]string, error) {
|
||||||
return filenames, nil
|
return filenames, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GraceActive returns whether the grace period (Cfg.GraceTime) is active
|
||||||
|
func GraceActive() bool {
|
||||||
|
return (60*time.Now().In(Cfg.Timezone).Hour() + time.Now().In(Cfg.Timezone).Minute()) <= int(Cfg.GraceTime.Minutes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TodayDate returns today's formatted date. It accounts for Config.GraceTime
|
||||||
|
func TodayDate() string {
|
||||||
|
dateFormatted := time.Now().In(Cfg.Timezone).Format(time.DateOnly)
|
||||||
|
if GraceActive() {
|
||||||
|
slog.Debug("grace period active",
|
||||||
|
"time", 60*time.Now().In(Cfg.Timezone).Hour()+time.Now().In(Cfg.Timezone).Minute(),
|
||||||
|
"grace", Cfg.GraceTime.Minutes())
|
||||||
|
dateFormatted = time.Now().In(Cfg.Timezone).AddDate(0, 0, -1).Format(time.DateOnly)
|
||||||
|
}
|
||||||
|
return dateFormatted
|
||||||
|
}
|
||||||
|
|
||||||
// ReadToday runs ReadFile with today's date as filename
|
// ReadToday runs ReadFile with today's date as filename
|
||||||
func ReadToday() ([]byte, error) {
|
func ReadToday() ([]byte, error) {
|
||||||
return ReadFile("day/" + time.Now().In(Cfg.Timezone).Format(time.DateOnly))
|
return ReadFile("day/" + TodayDate())
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveToday runs SaveFile with today's date as filename
|
// SaveToday runs SaveFile with today's date as filename
|
||||||
func SaveToday(contents []byte) error {
|
func SaveToday(contents []byte) error {
|
||||||
return SaveFile("day/"+time.Now().In(Cfg.Timezone).Format(time.DateOnly), contents)
|
return SaveFile("day/"+TodayDate(), contents)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,12 @@
|
||||||
"title.notes": "Notes",
|
"title.notes": "Notes",
|
||||||
|
|
||||||
"link.today": "today",
|
"link.today": "today",
|
||||||
|
"link.tomorrow": "tomorrow",
|
||||||
"link.days": "previous days",
|
"link.days": "previous days",
|
||||||
"link.notes": "notes",
|
"link.notes": "notes",
|
||||||
|
|
||||||
"description.notes": "/notes/<name> for a new note",
|
"description.notes": "/notes/<name> for a new note",
|
||||||
"misc.date": "Today is",
|
"time.date": "Today is",
|
||||||
|
"time.grace": "grace period active",
|
||||||
"button.save": "Save"
|
"button.save": "Save"
|
||||||
}
|
}
|
|
@ -6,10 +6,12 @@
|
||||||
"title.notes": "Заметки",
|
"title.notes": "Заметки",
|
||||||
|
|
||||||
"link.today": "сегодня",
|
"link.today": "сегодня",
|
||||||
|
"link.tomorrow": "завтра",
|
||||||
"link.days": "раньше",
|
"link.days": "раньше",
|
||||||
"link.notes": "заметки",
|
"link.notes": "заметки",
|
||||||
|
|
||||||
"description.notes": "/notes/<название> для новой заметки",
|
"description.notes": "/notes/<название> для новой заметки",
|
||||||
"misc.date": "Сегодня",
|
"time.date": "Сегодня",
|
||||||
|
"time.grace": "льготный период",
|
||||||
"button.save": "Сохранить"
|
"button.save": "Сохранить"
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
{{define "header"}}
|
{{define "header"}}
|
||||||
<header>
|
<header>
|
||||||
<h1>🌺 Hibiscus.txt</h1>
|
<h1>🌺 Hibiscus.txt</h1>
|
||||||
<p>{{translatableText "misc.date"}} <span id="today-date">a place</span></p>
|
<p>{{translatableText "time.date"}} <span id="today-date">a place</span> <span id="grace" hidden>({{ translatableText "time.grace" }})</span></p>
|
||||||
</header>
|
</header>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
@ -11,10 +11,11 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="manifest" href="/public/manifest.json" />
|
||||||
<link rel="icon" type="image/x-icon" href="/public/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="/public/favicon.ico">
|
||||||
<link rel="stylesheet" href="/public/main.css">
|
<link rel="stylesheet" href="/public/main.css">
|
||||||
<script src="/public/date.js"></script>
|
<script src="/public/date.js"></script>
|
||||||
<title>Hibiscus</title>
|
<title>Hibiscus.txt</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{{template "header" .}}
|
{{template "header" .}}
|
||||||
|
@ -22,7 +23,11 @@
|
||||||
{{template "main" .}}
|
{{template "main" .}}
|
||||||
</main>
|
</main>
|
||||||
{{template "footer" .}}
|
{{template "footer" .}}
|
||||||
<script defer>updateDate();beginDateUpdater()</script>
|
<script defer>
|
||||||
|
const langName="{{ translatableText "lang" }}";
|
||||||
|
const graceActive={{ graceActive }};
|
||||||
|
updateDate();beginDateUpdater()
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -1,18 +1,21 @@
|
||||||
// Format time in "Jan 02, 2006" format
|
// Format time in "Jan 02, 2006" format
|
||||||
function formatDate(date) {
|
function formatDate(date) {
|
||||||
let dateFormat = new Intl.DateTimeFormat('en', {
|
let dateFormat = new Intl.DateTimeFormat([langName, "en"], {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric'
|
day: 'numeric'
|
||||||
})
|
})
|
||||||
return dateFormat.format(Date.parse(date))
|
return dateFormat.format(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set today's date
|
// Set today's date
|
||||||
function updateDate() {
|
function updateDate() {
|
||||||
let todayDate = new Date()
|
|
||||||
let timeField = document.getElementById("today-date")
|
let timeField = document.getElementById("today-date")
|
||||||
timeField.innerText = formatDate(todayDate.toISOString().split('T')[0])
|
if (graceActive) {
|
||||||
|
let graceField = document.getElementById("grace")
|
||||||
|
graceField.hidden = false
|
||||||
|
}
|
||||||
|
timeField.innerText = formatDate(Date.now())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
22
public/manifest.json
Normal file
22
public/manifest.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"short_name": "Hibiscus",
|
||||||
|
"name": "Hibiscus.txt",
|
||||||
|
"description": "A plaintext diary",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/public/TODO.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/public/favicon.ico",
|
||||||
|
"type": "image/x-icon",
|
||||||
|
"sizes": "16x16"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "minimal-ui",
|
||||||
|
"scope": "/",
|
||||||
|
"background_color": "#f5f0e1",
|
||||||
|
"theme_color": "#f85552"
|
||||||
|
}
|
13
routes.go
13
routes.go
|
@ -25,7 +25,7 @@ type Entry struct {
|
||||||
|
|
||||||
type formatEntries func([]string) []Entry
|
type formatEntries func([]string) []Entry
|
||||||
|
|
||||||
var templateFuncs = map[string]interface{}{"translatableText": TranslatableText}
|
var templateFuncs = map[string]interface{}{"translatableText": TranslatableText, "graceActive": GraceActive}
|
||||||
var editTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFiles("./pages/base.html", "./pages/edit.html"))
|
var editTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFiles("./pages/base.html", "./pages/edit.html"))
|
||||||
var viewTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFiles("./pages/base.html", "./pages/entry.html"))
|
var viewTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFiles("./pages/base.html", "./pages/entry.html"))
|
||||||
var listTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFiles("./pages/base.html", "./pages/list.html"))
|
var listTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFiles("./pages/base.html", "./pages/list.html"))
|
||||||
|
@ -101,11 +101,16 @@ func GetDays(w http.ResponseWriter, r *http.Request) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
dayString = t.Format("02 Jan 2006")
|
dayString = t.Format("02 Jan 2006")
|
||||||
}
|
}
|
||||||
if v == time.Now().Format(time.DateOnly) {
|
|
||||||
// Fancy text for today
|
// Fancy text for today and tomorrow
|
||||||
// This looks bad, but strings.Title is deprecated, and I'm not importing a golang.org/x package for this...
|
// This looks bad, but strings.Title is deprecated, and I'm not importing a golang.org/x package for this...
|
||||||
|
// ( chances we ever run into tomorrow are really low)
|
||||||
|
if v == TodayDate() {
|
||||||
dayString = TranslatableText("link.today")
|
dayString = TranslatableText("link.today")
|
||||||
dayString = strings.ToTitle(string([]rune(dayString)[0])) + string([]rune(dayString)[1:])
|
dayString = strings.ToTitle(string([]rune(dayString)[0])) + string([]rune(dayString)[1:])
|
||||||
|
} else if GraceActive() && v > TodayDate() {
|
||||||
|
dayString = TranslatableText("link.tomorrow")
|
||||||
|
dayString = strings.ToTitle(string([]rune(dayString)[0])) + string([]rune(dayString)[1:])
|
||||||
}
|
}
|
||||||
filesFormatted = append(filesFormatted, Entry{Title: dayString, Link: "day/" + v})
|
filesFormatted = append(filesFormatted, Entry{Title: dayString, Link: "day/" + v})
|
||||||
}
|
}
|
||||||
|
@ -163,7 +168,7 @@ func GetDay(w http.ResponseWriter, r *http.Request) {
|
||||||
HandleWrite(w.Write([]byte("day not specified")))
|
HandleWrite(w.Write([]byte("day not specified")))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if dayString == time.Now().Format(time.DateOnly) { // Today can still be edited
|
if dayString == TodayDate() { // Today can still be edited
|
||||||
http.Redirect(w, r, "/", 302)
|
http.Redirect(w, r, "/", 302)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
19
serve.go
19
serve.go
|
@ -13,20 +13,23 @@ import (
|
||||||
func Serve() {
|
func Serve() {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Use(middleware.Logger, middleware.CleanPath, middleware.StripSlashes)
|
r.Use(middleware.Logger, middleware.CleanPath, middleware.StripSlashes)
|
||||||
r.Use(BasicAuth) // Is this good enough? Sure hope so
|
|
||||||
r.NotFound(NotFound)
|
r.NotFound(NotFound)
|
||||||
|
|
||||||
// Routes ==========
|
// Routes ==========
|
||||||
r.Get("/", GetToday)
|
userRouter := chi.NewRouter()
|
||||||
r.Post("/", PostToday)
|
userRouter.Use(BasicAuth)
|
||||||
r.Get("/day", GetDays)
|
userRouter.Get("/", GetToday)
|
||||||
r.Get("/day/{day}", GetDay)
|
userRouter.Post("/", PostToday)
|
||||||
r.Get("/notes", GetNotes)
|
userRouter.Get("/day", GetDays)
|
||||||
r.Get("/notes/{note}", GetNote)
|
userRouter.Get("/day/{day}", GetDay)
|
||||||
r.Post("/notes/{note}", PostNote)
|
userRouter.Get("/notes", GetNotes)
|
||||||
|
userRouter.Get("/notes/{note}", GetNote)
|
||||||
|
userRouter.Post("/notes/{note}", PostNote)
|
||||||
|
r.Mount("/", userRouter)
|
||||||
|
|
||||||
// API =============
|
// API =============
|
||||||
apiRouter := chi.NewRouter()
|
apiRouter := chi.NewRouter()
|
||||||
|
apiRouter.Use(BasicAuth)
|
||||||
apiRouter.Get("/readme", func(w http.ResponseWriter, r *http.Request) { GetFile("readme", w) })
|
apiRouter.Get("/readme", func(w http.ResponseWriter, r *http.Request) { GetFile("readme", w) })
|
||||||
apiRouter.Post("/readme", func(w http.ResponseWriter, r *http.Request) { PostFile("readme", w, r) })
|
apiRouter.Post("/readme", func(w http.ResponseWriter, r *http.Request) { PostFile("readme", w, r) })
|
||||||
apiRouter.Get("/day", func(w http.ResponseWriter, r *http.Request) { GetFileList("day", w) })
|
apiRouter.Get("/day", func(w http.ResponseWriter, r *http.Request) { GetFileList("day", w) })
|
||||||
|
|
Loading…
Reference in a new issue