Compare commits
No commits in common. "master" and "v1.11.0" have entirely different histories.
@ -1 +0,0 @@
|
|||||||
.git
|
|
||||||
28
.github/workflows/release.yaml
vendored
Normal file
28
.github/workflows/release.yaml
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
releases-matrix:
|
||||||
|
name: Release Go Binary
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
# build and publish in parallel a lot of binaries
|
||||||
|
# https://golang.org/doc/install/source#environment See supported Go OS/Arch pairs here
|
||||||
|
goos: [linux, darwin, openbsd, windows]
|
||||||
|
goarch: ["386", amd64, arm64]
|
||||||
|
exclude:
|
||||||
|
- goarch: "386"
|
||||||
|
goos: darwin
|
||||||
|
- goarch: arm64
|
||||||
|
goos: windows
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: wangyoucao577/go-release-action@v1.17
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
goos: ${{ matrix.goos }}
|
||||||
|
goarch: ${{ matrix.goarch }}
|
||||||
|
binary_name: "mycorrhiza"
|
||||||
|
extra_files: LICENSE README.md help/mycorrhiza.1
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,2 @@
|
|||||||
mycorrhiza
|
mycorrhiza
|
||||||
.idea
|
config.mk
|
||||||
|
|||||||
@ -1,85 +0,0 @@
|
|||||||
# Boilerplate in Mycorrhiza codebase
|
|
||||||
|
|
||||||
Being programmed by Go, mostly by Bouncepaw, the codebase contains a lot of boilerplate. This document is an attempt to describe how it is done.
|
|
||||||
|
|
||||||
## Modules
|
|
||||||
|
|
||||||
Mycorrhiza is arranged in quite many packages. They are thematic. For example, package `backlinks` has all things backlinks, including the storage, the views, and exported functions (not many, ideally). Such packages can be called modules, if you want.
|
|
||||||
|
|
||||||
## Views
|
|
||||||
|
|
||||||
Views are the biggest source of similar code. Before the transition from QTPL to Go's standard templates, this boilerplate energy was split differently, but was not instantly obvious. The current approach does not really introduce new boilerplate energy, but it does focus it, resulting in actual boilerplate. I hope you get the idea.
|
|
||||||
|
|
||||||
All related views are part of one module.
|
|
||||||
|
|
||||||
Views come in multiple parts.
|
|
||||||
|
|
||||||
The first part is the template itself. Call template files like that: `view_user_list.html`, prefixed with `view_`. The boilerplate is as follows.
|
|
||||||
|
|
||||||
```html
|
|
||||||
{{define "title"}}{{end}}
|
|
||||||
{{define "body"}}
|
|
||||||
{{end}}
|
|
||||||
```
|
|
||||||
|
|
||||||
More often than not, you will want to make template `title` a different template in the same file. See existing files for inspiration.
|
|
||||||
|
|
||||||
The code that makes those templates runnable lies in one file. This is the second part. It contains the following:.
|
|
||||||
|
|
||||||
The Russian translation is a `string` variable called `ruTranslation`; we currently have no other translations, but they are to be called like `frTranslation`, `eoTranslation`, et cetera.
|
|
||||||
|
|
||||||
|
|
||||||
```go
|
|
||||||
var (
|
|
||||||
ruTranslation = `
|
|
||||||
{{define "one thing"}}...{{end}}
|
|
||||||
{{define "other thing"}}...{{end}}
|
|
||||||
`
|
|
||||||
...
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Chains are collection of different language variants of the same template. Declare them and then assign them in a function, which you call somewhere (not just `init`!).
|
|
||||||
|
|
||||||
```go
|
|
||||||
var (
|
|
||||||
ruTranslation = `...`
|
|
||||||
chainStuff, chainAddStuff viewutil.Chain
|
|
||||||
)
|
|
||||||
|
|
||||||
func initViews() {
|
|
||||||
chainStuff = viewutil.CopyEnRuWith(fs, "view_stuff.html", ruTranslation)
|
|
||||||
chainAddStuff = viewutil.CopyEnRuWith(fs, "view_add_stuff.html", ruTranslation)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Then every view has a runner and its own datatype.
|
|
||||||
|
|
||||||
```go
|
|
||||||
//...
|
|
||||||
|
|
||||||
type dataStuff struct {
|
|
||||||
*viewutil.BaseData
|
|
||||||
StuffName string
|
|
||||||
}
|
|
||||||
|
|
||||||
func viewStuff(meta viewutil.Meta, stuffName string) {
|
|
||||||
viewutil.ExecutePage(meta, chainStuff, dataUserList{
|
|
||||||
BaseData: &viewutil.BaseData{},
|
|
||||||
StuffName: stuffName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Sometimes, two datatypes of different views are the same, it is ok to just share one, but name it so that it mentions both.
|
|
||||||
|
|
||||||
Avoid any logic in those runners. Keep them as boilerplate as they are. You rarely need to fill the `BaseData` field. Do it if you need to. If you don't, `viewutil.ExecutePage` will do its best to guess. We name the fields with capital letters.
|
|
||||||
|
|
||||||
### Troubleshooting
|
|
||||||
|
|
||||||
* Declared the chains?
|
|
||||||
* Assigned the chains?
|
|
||||||
* Assigned the chains, sure?
|
|
||||||
* Used the correct data type?
|
|
||||||
* `*viewutil.BaseData` is there?
|
|
||||||
* Used the correct chain?
|
|
||||||
@ -7,9 +7,6 @@ RUN go build -o /out/mycorrhiza .
|
|||||||
FROM alpine/git as app
|
FROM alpine/git as app
|
||||||
EXPOSE 1737
|
EXPOSE 1737
|
||||||
|
|
||||||
RUN apk add --no-cache curl
|
|
||||||
HEALTHCHECK CMD curl -Ns localhost:1737 || exit 1
|
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
RUN mkdir wiki
|
RUN mkdir wiki
|
||||||
COPY --from=build /out/mycorrhiza /usr/bin
|
COPY --from=build /out/mycorrhiza /usr/bin
|
||||||
|
|||||||
20
README.md
20
README.md
@ -2,17 +2,17 @@
|
|||||||
|
|
||||||
**Mycorrhiza Wiki** is a lightweight file-system wiki engine that uses Git for keeping history. [Main wiki](https://mycorrhiza.wiki)
|
**Mycorrhiza Wiki** is a lightweight file-system wiki engine that uses Git for keeping history. [Main wiki](https://mycorrhiza.wiki)
|
||||||
|
|
||||||
<img src="https://mycorrhiza.wiki/binary/release/1.15.1/screenshot" alt="A screenshot of mycorrhiza.wiki's home page in the Safari browser" width="700">
|
<img src="https://mycorrhiza.wiki/binary/release/1.10/screenshot" alt="A screenshot of mycorrhiza.wiki's home page in the Safari browser" width="700">
|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* **No database used.** Everything is stored in plain files. It makes installation super easy, and you can modify the content directly by yourself.
|
* **No database required.** Everything is stored in plain files. It makes installation super easy, and you can modify the content directly by yourself.
|
||||||
* **Everything is hyphae.** A hypha is a unit of content such as a picture, video or a text article. Hyphae can [transclude] and link each other, forming a tight network of hypertext pages.
|
* **Everything is hyphae.** A hypha is a unit of content such as a picture, video or a text article. Hyphae can [transclude] and link each other, forming a tight network of hypertext pages.
|
||||||
* **Hyphae are authored in [Mycomarkup],** a markup language that's designed to be unambiguous yet easy to use.
|
* **Hyphae are authored in [Mycomarkup],** a markup language that's designed to be unambigious yet easy to use.
|
||||||
* **Categories** let you organize hyphae without any hierarchy restrictions, with all the benefits of a category system.
|
* **Categories** let you organize hyphae without any hierarchy restrictions, with all the benefits of a category system.
|
||||||
* **Nesting of hyphae** is also supported if you like hierarchies.
|
* **Nesting of hyphae** is also supported if you like hierarchies.
|
||||||
* **History of changes.** Every change is safely stored in [Git]. Web feeds (RSS, Atom, JSON Feed) for recent changes included.
|
* **History of changes** for textual parts of hyphae. Every change is safely stored in [Git]. Web feeds for recent changes included.
|
||||||
* **Keyboard-driven navigation.** Press `?` to see the list of shortcuts.
|
* **Keyboard-driven navigation.** Press `?` to see the list of shortcuts.
|
||||||
* **Support for [authorization].** Both plain username-password pairs and [Telegram]'s login widget are supported.
|
* **Support for [authorization].** Both plain username-password pairs and [Telegram]'s login widget are supported.
|
||||||
* **[Open Graph] support.** The most relevant info about a hypha is made available through OG meta tags for consumption by other software.
|
* **[Open Graph] support.** The most relevant info about a hypha is made available through OG meta tags for consumption by other software.
|
||||||
@ -30,21 +30,17 @@ Compare Mycorrhiza Wiki with other engines on [WikiMatrix](https://www.wikimatri
|
|||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
|
|
||||||
See [the deployment guide](https://mycorrhiza.wiki/hypha/deployment) on the wiki. Also, Mycorrhiza might be available in your repositories. See [Repology](https://repology.org/project/mycorrhiza/versions).
|
See [the deployment guide](https://mycorrhiza.wiki/hypha/deployment) on the wiki.
|
||||||
|
|
||||||
|
|
||||||
## Contributing and community
|
## Contributing
|
||||||
|
|
||||||
|
* [SourceHut](https://sr.ht/~bouncepaw/mycorrhiza)
|
||||||
* [GitHub](https://github.com/bouncepaw/mycorrhiza)
|
* [GitHub](https://github.com/bouncepaw/mycorrhiza)
|
||||||
* [Fediverse @mycorrhiza@floss.social](https://floss.social/@mycorrhiza)
|
* [#mycorrhiza on irc.libera.chat](irc://irc.libera.chat/#mycorrhiza)
|
||||||
* Mirrors:
|
|
||||||
* [SourceHut](https://sr.ht/~bouncepaw/mycorrhiza)
|
|
||||||
* [Codeberg](https://codeberg.org/bouncepaw/mycorrhiza)
|
|
||||||
* [@mycorrhizadev (Russian) in Telegram](https://t.me/mycorrhizadev)
|
* [@mycorrhizadev (Russian) in Telegram](https://t.me/mycorrhizadev)
|
||||||
|
|
||||||
If you want to contribute with code, open a pull request on GitHub or send a patch to the [mailing list](https://lists.sr.ht/~bouncepaw/mycorrhiza-devel).
|
If you want to contribute with code, open a pull request on GitHub or send a patch to the [mailing list](https://lists.sr.ht/~bouncepaw/mycorrhiza-devel).
|
||||||
If you want to report an issue, open an issue on GitHub or contact us directly.
|
If you want to report an issue, open an issue on GitHub or contact us directly.
|
||||||
|
|
||||||
Consider supporting the development on [Boosty](https://boosty.to/bouncepaw).
|
Consider supporting the development on [Boosty](https://boosty.to/bouncepaw).
|
||||||
|
|
||||||
Check out [Betula](https://betula.mycorrhiza.wiki) as well.
|
|
||||||
158
admin/admin.go
Normal file
158
admin/admin.go
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/viewutil"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/l18n"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/user"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/util"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/views"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handlerAdmin provides the admin panel.
|
||||||
|
func handlerAdmin(w http.ResponseWriter, rq *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html;charset=utf-8")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
viewPanel(viewutil.MetaFrom(w, rq))
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlerAdminShutdown kills the wiki.
|
||||||
|
func handlerAdminShutdown(w http.ResponseWriter, rq *http.Request) {
|
||||||
|
if user.CanProceed(rq, "admin/shutdown") {
|
||||||
|
log.Println("An admin commanded the wiki to shutdown")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlerAdminReindexUsers reinitialises the user system.
|
||||||
|
func handlerAdminReindexUsers(w http.ResponseWriter, rq *http.Request) {
|
||||||
|
user.ReadUsersFromFilesystem()
|
||||||
|
redirectTo := rq.Referer()
|
||||||
|
if redirectTo == "" {
|
||||||
|
redirectTo = "/hypha/" + cfg.UserHypha
|
||||||
|
}
|
||||||
|
http.Redirect(w, rq, redirectTo, http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerAdminUsers(w http.ResponseWriter, rq *http.Request) {
|
||||||
|
// Get a sorted list of users
|
||||||
|
var users []*user.User
|
||||||
|
for u := range user.YieldUsers() {
|
||||||
|
users = append(users, u)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(users, func(i, j int) bool {
|
||||||
|
less := users[i].RegisteredAt.Before(users[j].RegisteredAt)
|
||||||
|
return less
|
||||||
|
})
|
||||||
|
viewList(viewutil.MetaFrom(w, rq), users)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerAdminUserEdit(w http.ResponseWriter, rq *http.Request) {
|
||||||
|
vars := mux.Vars(rq)
|
||||||
|
u := user.ByName(vars["username"])
|
||||||
|
if u == nil {
|
||||||
|
util.HTTP404Page(w, "404 page not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f := util.FormDataFromRequest(rq, []string{"group"})
|
||||||
|
|
||||||
|
if rq.Method == http.MethodPost {
|
||||||
|
oldGroup := u.Group
|
||||||
|
newGroup := f.Get("group")
|
||||||
|
|
||||||
|
if user.ValidGroup(newGroup) {
|
||||||
|
u.Group = newGroup
|
||||||
|
if err := user.SaveUserDatabase(); err != nil {
|
||||||
|
u.Group = oldGroup
|
||||||
|
log.Println(err)
|
||||||
|
f = f.WithError(err)
|
||||||
|
} else {
|
||||||
|
http.Redirect(w, rq, "/admin/users/", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
f = f.WithError(fmt.Errorf("invalid group ‘%s’", newGroup))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Put("group", u.Group)
|
||||||
|
|
||||||
|
var lc = l18n.FromRequest(rq)
|
||||||
|
html := views.AdminUserEdit(u, f, lc)
|
||||||
|
html = views.Base(viewutil.MetaFrom(w, rq), fmt.Sprintf(lc.Get("admin.user_title"), u.Name), html)
|
||||||
|
|
||||||
|
if f.HasError() {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
|
||||||
|
io.WriteString(w, html)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerAdminUserDelete(w http.ResponseWriter, rq *http.Request) {
|
||||||
|
vars := mux.Vars(rq)
|
||||||
|
u := user.ByName(vars["username"])
|
||||||
|
if u == nil {
|
||||||
|
util.HTTP404Page(w, "404 page not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f := util.NewFormData()
|
||||||
|
|
||||||
|
if rq.Method == http.MethodPost {
|
||||||
|
f = f.WithError(user.DeleteUser(u.Name))
|
||||||
|
if !f.HasError() {
|
||||||
|
http.Redirect(w, rq, "/admin/users/", http.StatusSeeOther)
|
||||||
|
} else {
|
||||||
|
log.Println(f.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lc = l18n.FromRequest(rq)
|
||||||
|
html := views.AdminUserDelete(u, util.NewFormData(), lc)
|
||||||
|
html = views.Base(viewutil.MetaFrom(w, rq), fmt.Sprintf(lc.Get("admin.user_title"), u.Name), html)
|
||||||
|
|
||||||
|
if f.HasError() {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
|
||||||
|
io.WriteString(w, html)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerAdminUserNew(w http.ResponseWriter, rq *http.Request) {
|
||||||
|
var lc = l18n.FromRequest(rq)
|
||||||
|
if rq.Method == http.MethodGet {
|
||||||
|
// New user form
|
||||||
|
html := views.AdminUserNew(util.NewFormData(), lc)
|
||||||
|
html = views.Base(viewutil.MetaFrom(w, rq), lc.Get("admin.newuser_title"), html)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
|
||||||
|
io.WriteString(w, html)
|
||||||
|
} else if rq.Method == http.MethodPost {
|
||||||
|
// Create a user
|
||||||
|
f := util.FormDataFromRequest(rq, []string{"name", "password", "group"})
|
||||||
|
|
||||||
|
err := user.Register(f.Get("name"), f.Get("password"), f.Get("group"), "local", true)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
html := views.AdminUserNew(f.WithError(err), lc)
|
||||||
|
html = views.Base(viewutil.MetaFrom(w, rq), lc.Get("admin.newuser_title"), html)
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
|
||||||
|
io.WriteString(w, html)
|
||||||
|
} else {
|
||||||
|
http.Redirect(w, rq, "/admin/users/", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
admin/view.go
Normal file
71
admin/view.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/user"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/viewutil"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const adminTranslationRu = `
|
||||||
|
{{define "panel title"}}Панель админстратора{{end}}
|
||||||
|
{{define "panel safe section title"}}Безопасная секция{{end}}
|
||||||
|
{{define "panel link about"}}Об этой вики{{end}}
|
||||||
|
{{define "panel update header"}}Обновить ссылки в верхней панели{{end}}
|
||||||
|
{{define "panel link user list"}}Список пользователей{{end}}
|
||||||
|
{{define "panel users"}}Управление пользователями{{end}}
|
||||||
|
{{define "panel unsafe section title"}}Опасная секция{{end}}
|
||||||
|
{{define "panel shutdown"}}Выключить вики{{end}}
|
||||||
|
{{define "panel reindex hyphae"}}Переиндексировать гифы{{end}}
|
||||||
|
|
||||||
|
{{define "manage users"}}Управление пользователями{{end}}
|
||||||
|
{{define "create user"}}Создать пользователя{{end}}
|
||||||
|
{{define "reindex users"}}Переиндексировать пользователей{{end}}
|
||||||
|
{{define "name"}}Имя{{end}}
|
||||||
|
{{define "group"}}Группа{{end}}
|
||||||
|
{{define "registered at"}}Зарегистрирован{{end}}
|
||||||
|
{{define "actions"}}Действия{{end}}
|
||||||
|
{{define "edit"}}Изменить{{end}}
|
||||||
|
`
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed *.html
|
||||||
|
fs embed.FS
|
||||||
|
panelChain viewutil.Chain
|
||||||
|
listChain viewutil.Chain
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init(rtr *mux.Router) {
|
||||||
|
rtr.HandleFunc("/shutdown", handlerAdminShutdown).Methods(http.MethodPost)
|
||||||
|
rtr.HandleFunc("/reindex-users", handlerAdminReindexUsers).Methods(http.MethodPost)
|
||||||
|
|
||||||
|
rtr.HandleFunc("/new-user", handlerAdminUserNew).Methods(http.MethodGet, http.MethodPost)
|
||||||
|
rtr.HandleFunc("/users/{username}/edit", handlerAdminUserEdit).Methods(http.MethodGet, http.MethodPost)
|
||||||
|
rtr.HandleFunc("/users/{username}/delete", handlerAdminUserDelete).Methods(http.MethodGet, http.MethodPost)
|
||||||
|
rtr.HandleFunc("/users", handlerAdminUsers)
|
||||||
|
|
||||||
|
rtr.HandleFunc("/", handlerAdmin)
|
||||||
|
|
||||||
|
panelChain = viewutil.CopyEnRuWith(fs, "view_panel.html", adminTranslationRu)
|
||||||
|
listChain = viewutil.CopyEnRuWith(fs, "view_user_list.html", adminTranslationRu)
|
||||||
|
}
|
||||||
|
|
||||||
|
func viewPanel(meta viewutil.Meta) {
|
||||||
|
viewutil.ExecutePage(meta, panelChain, &viewutil.BaseData{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type listData struct {
|
||||||
|
*viewutil.BaseData
|
||||||
|
UserHypha string
|
||||||
|
Users []*user.User
|
||||||
|
}
|
||||||
|
|
||||||
|
func viewList(meta viewutil.Meta, users []*user.User) {
|
||||||
|
viewutil.ExecutePage(meta, listChain, listData{
|
||||||
|
BaseData: &viewutil.BaseData{},
|
||||||
|
UserHypha: cfg.UserHypha,
|
||||||
|
Users: users,
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -10,8 +10,6 @@
|
|||||||
<li><a href="/update-header-links">{{block "panel update header" .}}Reload header links{{end}}</a></li>
|
<li><a href="/update-header-links">{{block "panel update header" .}}Reload header links{{end}}</a></li>
|
||||||
<li><a href="/user-list">{{block "panel link user list" .}}User list{{end}}</a></li>
|
<li><a href="/user-list">{{block "panel link user list" .}}User list{{end}}</a></li>
|
||||||
<li><a href="/admin/users/">{{block "panel users" .}}Manage users{{end}}</a></li>
|
<li><a href="/admin/users/">{{block "panel users" .}}Manage users{{end}}</a></li>
|
||||||
<li><a href="/interwiki">{{block "panel interwiki" .}}Interwiki{{end}}</a></li>
|
|
||||||
<li><a href="/orphans">{{block "panel/orphans" .}}Orphaned hyphae{{end}}</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
@ -2,11 +2,10 @@
|
|||||||
package backlinks
|
package backlinks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
|
"log"
|
||||||
"log/slog"
|
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
|
||||||
|
|
||||||
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
"github.com/bouncepaw/mycorrhiza/util"
|
"github.com/bouncepaw/mycorrhiza/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -54,33 +53,14 @@ func IndexBacklinks() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BacklinksCount returns the amount of backlinks to the hypha. Pass canonical names.
|
// BacklinksCount returns the amount of backlinks to the hypha.
|
||||||
func BacklinksCount(hyphaName string) int {
|
func BacklinksCount(h hyphae.Hypha) int {
|
||||||
if links, exists := backlinkIndex[hyphaName]; exists {
|
if links, exists := backlinkIndex[h.CanonicalName()]; exists {
|
||||||
return len(links)
|
return len(links)
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func BacklinksFor(hyphaName string) []string {
|
|
||||||
var backlinks []string
|
|
||||||
for b := range yieldHyphaBacklinks(hyphaName) {
|
|
||||||
backlinks = append(backlinks, b)
|
|
||||||
}
|
|
||||||
return backlinks
|
|
||||||
}
|
|
||||||
|
|
||||||
func Orphans() []string {
|
|
||||||
var orphans []string
|
|
||||||
for h := range hyphae.YieldExistingHyphae() {
|
|
||||||
if BacklinksCount(h.CanonicalName()) == 0 {
|
|
||||||
orphans = append(orphans, h.CanonicalName())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Strings(orphans)
|
|
||||||
return orphans
|
|
||||||
}
|
|
||||||
|
|
||||||
// Using set here seems like the most appropriate solution
|
// Using set here seems like the most appropriate solution
|
||||||
type linkSet map[string]struct{}
|
type linkSet map[string]struct{}
|
||||||
|
|
||||||
@ -108,7 +88,7 @@ func fetchText(h hyphae.Hypha) string {
|
|||||||
|
|
||||||
text, err := os.ReadFile(path)
|
text, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to read file", "path", path, "err", err, "hyphaName", h.CanonicalName())
|
log.Println(err)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return string(text)
|
return string(text)
|
||||||
@ -1,13 +1,12 @@
|
|||||||
package backlinks
|
package backlinks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
|
"github.com/bouncepaw/mycomarkup/v5"
|
||||||
|
"github.com/bouncepaw/mycomarkup/v5/links"
|
||||||
|
"github.com/bouncepaw/mycomarkup/v5/mycocontext"
|
||||||
|
"github.com/bouncepaw/mycomarkup/v5/tools"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
"github.com/bouncepaw/mycorrhiza/mycoopts"
|
"github.com/bouncepaw/mycorrhiza/mycoopts"
|
||||||
|
|
||||||
"git.sr.ht/~bouncepaw/mycomarkup/v5"
|
|
||||||
"git.sr.ht/~bouncepaw/mycomarkup/v5/links"
|
|
||||||
"git.sr.ht/~bouncepaw/mycomarkup/v5/mycocontext"
|
|
||||||
"git.sr.ht/~bouncepaw/mycomarkup/v5/tools"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// UpdateBacklinksAfterEdit is a creation/editing hook for backlinks index
|
// UpdateBacklinksAfterEdit is a creation/editing hook for backlinks index
|
||||||
85
backlinks/web.go
Normal file
85
backlinks/web.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package backlinks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/util"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/viewutil"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitHandlers(rtr *mux.Router) {
|
||||||
|
rtr.PathPrefix("/backlinks/").HandlerFunc(handlerBacklinks)
|
||||||
|
rtr.PathPrefix("/orphans").HandlerFunc(handlerOrphans)
|
||||||
|
chainBacklinks = viewutil.CopyEnRuWith(fs, "view_backlinks.html", ruTranslation)
|
||||||
|
chainOrphans = viewutil.CopyEnRuWith(fs, "view_orphans.html", ruTranslation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlerBacklinks lists all backlinks to a hypha.
|
||||||
|
func handlerBacklinks(w http.ResponseWriter, rq *http.Request) {
|
||||||
|
var (
|
||||||
|
hyphaName = util.HyphaNameFromRq(rq, "backlinks")
|
||||||
|
backlinks []string
|
||||||
|
)
|
||||||
|
for b := range yieldHyphaBacklinks(hyphaName) {
|
||||||
|
backlinks = append(backlinks, b)
|
||||||
|
}
|
||||||
|
viewBacklinks(viewutil.MetaFrom(w, rq), hyphaName, backlinks)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerOrphans(w http.ResponseWriter, rq *http.Request) {
|
||||||
|
var orphans []string
|
||||||
|
for h := range hyphae.YieldExistingHyphae() {
|
||||||
|
if BacklinksCount(h) == 0 {
|
||||||
|
orphans = append(orphans, h.CanonicalName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(orphans)
|
||||||
|
viewOrphans(viewutil.MetaFrom(w, rq), orphans)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed *.html
|
||||||
|
fs embed.FS
|
||||||
|
ruTranslation = `
|
||||||
|
{{define "backlinks to text"}}Обратные ссылки на {{.}}{{end}}
|
||||||
|
{{define "backlinks to link"}}Обратные ссылки на <a href="/hypha/{{.}}">{{beautifulName .}}</a>{{end}}
|
||||||
|
{{define "description"}}Ниже перечислены гифы, на которых есть ссылка на эту гифу, трансклюзия этой гифы или эта гифа вставлена как изображение.{{end}}
|
||||||
|
{{define "orphaned hyphae"}}Гифы-сироты{{end}}
|
||||||
|
{{define "orphan description"}}Ниже перечислены гифы без ссылок на них.{{end}}
|
||||||
|
`
|
||||||
|
chainBacklinks viewutil.Chain
|
||||||
|
chainOrphans viewutil.Chain
|
||||||
|
)
|
||||||
|
|
||||||
|
type backlinksData struct {
|
||||||
|
*viewutil.BaseData
|
||||||
|
HyphaName string
|
||||||
|
Backlinks []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func viewBacklinks(meta viewutil.Meta, hyphaName string, backlinks []string) {
|
||||||
|
viewutil.ExecutePage(meta, chainBacklinks, backlinksData{
|
||||||
|
BaseData: &viewutil.BaseData{
|
||||||
|
Addr: "/backlinks/" + hyphaName,
|
||||||
|
},
|
||||||
|
HyphaName: hyphaName,
|
||||||
|
Backlinks: backlinks,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type orphansData struct {
|
||||||
|
*viewutil.BaseData
|
||||||
|
Orphans []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func viewOrphans(meta viewutil.Meta, orphans []string) {
|
||||||
|
viewutil.ExecutePage(meta, chainOrphans, orphansData{
|
||||||
|
BaseData: &viewutil.BaseData{
|
||||||
|
Addr: "/orphans",
|
||||||
|
},
|
||||||
|
Orphans: orphans,
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -23,8 +23,8 @@ package categories
|
|||||||
|
|
||||||
import "sync"
|
import "sync"
|
||||||
|
|
||||||
// ListOfCategories returns unsorted names of all categories.
|
// listOfCategories returns unsorted names of all categories.
|
||||||
func ListOfCategories() (categoryList []string) {
|
func listOfCategories() (categoryList []string) {
|
||||||
mutex.RLock()
|
mutex.RLock()
|
||||||
for cat, _ := range categoryToHyphae {
|
for cat, _ := range categoryToHyphae {
|
||||||
categoryList = append(categoryList, cat)
|
categoryList = append(categoryList, cat)
|
||||||
@ -33,8 +33,8 @@ func ListOfCategories() (categoryList []string) {
|
|||||||
return categoryList
|
return categoryList
|
||||||
}
|
}
|
||||||
|
|
||||||
// CategoriesWithHypha returns what categories have the given hypha. The hypha name must be canonical.
|
// categoriesWithHypha returns what categories have the given hypha. The hypha name must be canonical.
|
||||||
func CategoriesWithHypha(hyphaName string) (categoryList []string) {
|
func categoriesWithHypha(hyphaName string) (categoryList []string) {
|
||||||
mutex.RLock()
|
mutex.RLock()
|
||||||
defer mutex.RUnlock()
|
defer mutex.RUnlock()
|
||||||
if node, ok := hyphaToCategories[hyphaName]; ok {
|
if node, ok := hyphaToCategories[hyphaName]; ok {
|
||||||
@ -44,8 +44,8 @@ func CategoriesWithHypha(hyphaName string) (categoryList []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HyphaeInCategory returns what hyphae are in the category. If the returned slice is empty, the category does not exist, and vice versa. The category name must be canonical.
|
// hyphaeInCategory returns what hyphae are in the category. If the returned slice is empty, the category does not exist, and vice versa. The category name must be canonical.
|
||||||
func HyphaeInCategory(catName string) (hyphaList []string) {
|
func hyphaeInCategory(catName string) (hyphaList []string) {
|
||||||
mutex.RLock()
|
mutex.RLock()
|
||||||
defer mutex.RUnlock()
|
defer mutex.RUnlock()
|
||||||
if node, ok := categoryToHyphae[catName]; ok {
|
if node, ok := categoryToHyphae[catName]; ok {
|
||||||
@ -75,8 +75,8 @@ func AddHyphaToCategory(hyphaName, catName string) {
|
|||||||
go saveToDisk()
|
go saveToDisk()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveHyphaFromCategory removes the hypha from the category and updates the records on the disk. If the hypha is not in the category, nothing happens. Pass canonical names.
|
// removeHyphaFromCategory removes the hypha from the category and updates the records on the disk. If the hypha is not in the category, nothing happens. Pass canonical names.
|
||||||
func RemoveHyphaFromCategory(hyphaName, catName string) {
|
func removeHyphaFromCategory(hyphaName, catName string) {
|
||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
if node, ok := hyphaToCategories[hyphaName]; ok {
|
if node, ok := hyphaToCategories[hyphaName]; ok {
|
||||||
node.removeCategory(catName)
|
node.removeCategory(catName)
|
||||||
@ -95,29 +95,6 @@ func RemoveHyphaFromCategory(hyphaName, catName string) {
|
|||||||
go saveToDisk()
|
go saveToDisk()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveHyphaFromAllCategories removes the given hypha from all the categories.
|
|
||||||
func RemoveHyphaFromAllCategories(hyphaName string) {
|
|
||||||
cats := CategoriesWithHypha(hyphaName)
|
|
||||||
mutex.Lock()
|
|
||||||
defer mutex.Unlock()
|
|
||||||
for _, cat := range cats {
|
|
||||||
if node, ok := hyphaToCategories[hyphaName]; ok {
|
|
||||||
node.removeCategory(cat)
|
|
||||||
if len(node.categoryList) == 0 {
|
|
||||||
delete(hyphaToCategories, hyphaName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if node, ok := categoryToHyphae[cat]; ok {
|
|
||||||
node.removeHypha(hyphaName)
|
|
||||||
if len(node.hyphaList) == 0 {
|
|
||||||
delete(categoryToHyphae, cat)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
go saveToDisk()
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenameHyphaInAllCategories finds all mentions of oldName and replaces them with newName. Pass canonical names. Make sure newName is not taken. If oldName is not in any category, RenameHyphaInAllCategories is a no-op.
|
// RenameHyphaInAllCategories finds all mentions of oldName and replaces them with newName. Pass canonical names. Make sure newName is not taken. If oldName is not in any category, RenameHyphaInAllCategories is a no-op.
|
||||||
func RenameHyphaInAllCategories(oldName, newName string) {
|
func RenameHyphaInAllCategories(oldName, newName string) {
|
||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
@ -2,25 +2,25 @@ package categories
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log/slog"
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/util"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/files"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var categoryToHyphae = map[string]*categoryNode{}
|
var categoryToHyphae = map[string]*categoryNode{}
|
||||||
var hyphaToCategories = map[string]*hyphaNode{}
|
var hyphaToCategories = map[string]*hyphaNode{}
|
||||||
|
|
||||||
// Init initializes the category system. Call it after the Structure is initialized. This function might terminate the program in case of a bad mood or filesystem faults.
|
// Init initializes the category system. Call it after the Structure is initialized. This function might terminate the program in case of a bad mood or filesystem faults.
|
||||||
func Init() error {
|
func Init() {
|
||||||
record, err := readCategoriesFromDisk()
|
var (
|
||||||
|
record, err = readCategoriesFromDisk()
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to read categories from disk", "err", err)
|
log.Fatalln(err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cat := range record.Categories {
|
for _, cat := range record.Categories {
|
||||||
@ -45,8 +45,7 @@ func Init() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("Indexed categories", "n", len(categoryToHyphae))
|
log.Println("Found", len(categoryToHyphae), "categories")
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type categoryNode struct {
|
type categoryNode struct {
|
||||||
@ -123,7 +122,9 @@ func readCategoriesFromDisk() (catFileRecord, error) {
|
|||||||
var fileMutex sync.Mutex
|
var fileMutex sync.Mutex
|
||||||
|
|
||||||
func saveToDisk() {
|
func saveToDisk() {
|
||||||
var record catFileRecord
|
var (
|
||||||
|
record catFileRecord
|
||||||
|
)
|
||||||
for name, node := range categoryToHyphae {
|
for name, node := range categoryToHyphae {
|
||||||
record.Categories = append(record.Categories, catRecord{
|
record.Categories = append(record.Categories, catRecord{
|
||||||
Name: name,
|
Name: name,
|
||||||
@ -132,16 +133,13 @@ func saveToDisk() {
|
|||||||
}
|
}
|
||||||
data, err := json.MarshalIndent(record, "", "\t")
|
data, err := json.MarshalIndent(record, "", "\t")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to marshal categories record", "err", err)
|
log.Fatalln(err) // Better fail now, than later
|
||||||
os.Exit(1) // Better fail now, than later
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make the data safer somehow?? Back it up before overwriting?
|
// TODO: make the data safer somehow?? Back it up before overwriting?
|
||||||
fileMutex.Lock()
|
fileMutex.Lock()
|
||||||
err = os.WriteFile(files.CategoriesJSON(), data, 0666)
|
err = os.WriteFile(files.CategoriesJSON(), data, 0666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to write categories.json", "err", err)
|
log.Fatalln(err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
fileMutex.Unlock()
|
fileMutex.Unlock()
|
||||||
}
|
}
|
||||||
@ -1,45 +1,40 @@
|
|||||||
package web
|
package categories
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"github.com/bouncepaw/mycorrhiza/user"
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/categories"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/user"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/util"
|
"github.com/bouncepaw/mycorrhiza/util"
|
||||||
"github.com/bouncepaw/mycorrhiza/web/viewutil"
|
"github.com/bouncepaw/mycorrhiza/viewutil"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// InitHandlers initializes HTTP handlers for the given router. Call somewhere in package web.
|
||||||
|
func InitHandlers(r *mux.Router) {
|
||||||
|
r.PathPrefix("/add-to-category").HandlerFunc(handlerAddToCategory).Methods("POST")
|
||||||
|
r.PathPrefix("/remove-from-category").HandlerFunc(handlerRemoveFromCategory).Methods("POST")
|
||||||
|
r.PathPrefix("/category/").HandlerFunc(handlerCategory).Methods("GET")
|
||||||
|
r.PathPrefix("/edit-category/").HandlerFunc(handlerEditCategory).Methods("GET")
|
||||||
|
r.PathPrefix("/category").HandlerFunc(handlerListCategory).Methods("GET")
|
||||||
|
prepareViews()
|
||||||
|
}
|
||||||
|
|
||||||
func handlerEditCategory(w http.ResponseWriter, rq *http.Request) {
|
func handlerEditCategory(w http.ResponseWriter, rq *http.Request) {
|
||||||
util.PrepareRq(rq)
|
util.PrepareRq(rq)
|
||||||
meta := viewutil.MetaFrom(w, rq)
|
|
||||||
catName := util.CanonicalName(strings.TrimPrefix(strings.TrimPrefix(rq.URL.Path, "/edit-category"), "/"))
|
catName := util.CanonicalName(strings.TrimPrefix(strings.TrimPrefix(rq.URL.Path, "/edit-category"), "/"))
|
||||||
if catName == "" {
|
if catName == "" {
|
||||||
viewutil.HandlerNotFound(w, rq)
|
http.Error(w, "No category name", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Println("Editing category", catName)
|
||||||
slog.Info("Editing category", "name", catName)
|
categoryEdit(viewutil.MetaFrom(w, rq), catName)
|
||||||
_ = pageCatEdit.RenderTo(meta, map[string]any{
|
|
||||||
"Addr": "/edit-category/" + catName,
|
|
||||||
"CatName": catName,
|
|
||||||
"Hyphae": categories.HyphaeInCategory(catName),
|
|
||||||
"GivenPermissionToModify": meta.U.CanProceed("add-to-category"),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlerListCategory(w http.ResponseWriter, rq *http.Request) {
|
func handlerListCategory(w http.ResponseWriter, rq *http.Request) {
|
||||||
slog.Info("Viewing list of categories")
|
log.Println("Viewing list of categories")
|
||||||
cats := categories.ListOfCategories()
|
categoryList(viewutil.MetaFrom(w, rq))
|
||||||
sort.Strings(cats)
|
|
||||||
|
|
||||||
_ = pageCatList.RenderTo(viewutil.MetaFrom(w, rq), map[string]any{
|
|
||||||
"Addr": "/category",
|
|
||||||
"Categories": cats,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlerCategory(w http.ResponseWriter, rq *http.Request) {
|
func handlerCategory(w http.ResponseWriter, rq *http.Request) {
|
||||||
@ -49,15 +44,8 @@ func handlerCategory(w http.ResponseWriter, rq *http.Request) {
|
|||||||
handlerListCategory(w, rq)
|
handlerListCategory(w, rq)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Println("Viewing category", catName)
|
||||||
meta := viewutil.MetaFrom(w, rq)
|
categoryPage(viewutil.MetaFrom(w, rq), catName)
|
||||||
slog.Info("Viewing category", "name", catName)
|
|
||||||
_ = pageCatPage.RenderTo(meta, map[string]any{
|
|
||||||
"Addr": "/category/" + catName,
|
|
||||||
"CatName": catName,
|
|
||||||
"Hyphae": categories.HyphaeInCategory(catName),
|
|
||||||
"GivenPermissionToModify": meta.U.CanProceed("add-to-category"),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A request for removal of hyphae can either remove one hypha (used in the card on /hypha) or many hyphae (used in /edit-category). Both approaches are handled by /remove-from-category. This function finds all passed hyphae.
|
// A request for removal of hyphae can either remove one hypha (used in the card on /hypha) or many hyphae (used in /edit-category). Both approaches are handled by /remove-from-category. This function finds all passed hyphae.
|
||||||
@ -65,7 +53,7 @@ func handlerCategory(w http.ResponseWriter, rq *http.Request) {
|
|||||||
// There is one hypha from the hypha field. Then there are n hyphae in fields prefixed by _. It seems like I have to do it myself. Compare with PHP which handles it for you. I hope I am doing this wrong.
|
// There is one hypha from the hypha field. Then there are n hyphae in fields prefixed by _. It seems like I have to do it myself. Compare with PHP which handles it for you. I hope I am doing this wrong.
|
||||||
func hyphaeFromRequest(rq *http.Request) (canonicalNames []string) {
|
func hyphaeFromRequest(rq *http.Request) (canonicalNames []string) {
|
||||||
if err := rq.ParseForm(); err != nil {
|
if err := rq.ParseForm(); err != nil {
|
||||||
slog.Info("Failed to parse form", "err", err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
if hyphaName := util.CanonicalName(rq.PostFormValue("hypha")); hyphaName != "" {
|
if hyphaName := util.CanonicalName(rq.PostFormValue("hypha")); hyphaName != "" {
|
||||||
canonicalNames = append(canonicalNames, hyphaName)
|
canonicalNames = append(canonicalNames, hyphaName)
|
||||||
@ -99,17 +87,15 @@ func handlerRemoveFromCategory(w http.ResponseWriter, rq *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(hyphaNames) == 0 || catName == "" {
|
if len(hyphaNames) == 0 || catName == "" {
|
||||||
slog.Info("No data for removal of hyphae from category passed",
|
log.Printf("%s passed no data for removal of hyphae from a category\n", u.Name)
|
||||||
"username", u.Name, "catName", catName)
|
|
||||||
http.Redirect(w, rq, redirectTo, http.StatusSeeOther)
|
http.Redirect(w, rq, redirectTo, http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, hyphaName := range hyphaNames {
|
for _, hyphaName := range hyphaNames {
|
||||||
// TODO: Make it more effective.
|
// TODO: Make it more effective.
|
||||||
categories.RemoveHyphaFromCategory(hyphaName, catName)
|
removeHyphaFromCategory(hyphaName, catName)
|
||||||
}
|
}
|
||||||
slog.Info("Remove hyphae from category",
|
log.Printf("%s removed %q from category %s\n", u.Name, hyphaNames, catName)
|
||||||
"username", u.Name, "catName", catName, "hyphaNames", hyphaNames)
|
|
||||||
http.Redirect(w, rq, redirectTo, http.StatusSeeOther)
|
http.Redirect(w, rq, redirectTo, http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,7 +115,7 @@ func handlerAddToCategory(w http.ResponseWriter, rq *http.Request) {
|
|||||||
http.Redirect(w, rq, redirectTo, http.StatusSeeOther)
|
http.Redirect(w, rq, redirectTo, http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
slog.Info(user.FromRequest(rq).Name, "added", hyphaName, "to", catName)
|
log.Println(user.FromRequest(rq).Name, "added", hyphaName, "to", catName)
|
||||||
categories.AddHyphaToCategory(hyphaName, catName)
|
AddHyphaToCategory(hyphaName, catName)
|
||||||
http.Redirect(w, rq, redirectTo, http.StatusSeeOther)
|
http.Redirect(w, rq, redirectTo, http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
36
categories/view_card.html
Normal file
36
categories/view_card.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{{define "category card"}}
|
||||||
|
{{if or .GivenPermissionToModify (len .Categories)}}
|
||||||
|
{{$hyphaName := .HyphaName}}
|
||||||
|
{{$givenPermission := .GivenPermissionToModify}}
|
||||||
|
<aside class="layout-card categories-card">
|
||||||
|
<h2 class="layout-card__title">{{block `categories` .}}Categories{{end}}</h2>
|
||||||
|
<ul class="categories-card__entries">
|
||||||
|
{{range .Categories}}
|
||||||
|
<li class="categories-card__entry">
|
||||||
|
<a class="categories-card__link" href="/category/{{.}}">{{beautifulName .}}</a>
|
||||||
|
<form method="POST" action="/remove-from-category" class="categories-card__remove-form">
|
||||||
|
<input type="hidden" name="cat" value="{{.}}">
|
||||||
|
<input type="hidden" name="hypha" value="{{$hyphaName}}">
|
||||||
|
<input type="hidden" name="redirect-to" value="/hypha/{{$hyphaName}}">
|
||||||
|
{{if $givenPermission}}
|
||||||
|
<input type="submit" value="x" class="btn categories-card__btn"
|
||||||
|
title="{{block `remove from category title` .}}Remove the hypha from this category{{end}}">
|
||||||
|
{{end}}
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
{{if .GivenPermissionToModify}}
|
||||||
|
<li class="categories-card__entry categories-card__add-to-cat">
|
||||||
|
<form method="POST" action="/add-to-category" class="categories-card__add-form">
|
||||||
|
<input type="text" name="cat" id="_cat-name"
|
||||||
|
placeholder="{{block `placeholder` .}}Category name...{{end}}">
|
||||||
|
<input type="hidden" name="hypha" value="{{$hyphaName}}">
|
||||||
|
<input type="hidden" name="redirect-to" value="/hypha/{{$hyphaName}}">
|
||||||
|
<input type="submit" class="btn categories-card__btn" value="+"
|
||||||
|
title="{{block `add to category title` .}}Add the hypha to this category{{end}}">
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
{{end}}{{end}}
|
||||||
37
categories/view_edit.html
Normal file
37
categories/view_edit.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{{define "edit category x"}}Edit category {{beautifulName .}}{{end}}
|
||||||
|
{{define "title"}}{{template "edit category x" .CatName}}{{end}}
|
||||||
|
{{define "body"}}
|
||||||
|
<main class="main-width category">
|
||||||
|
<h1>{{block "edit category heading" .CatName}}Edit category <a href="/category/{{.}}">{{beautifulName .}}</a>{{end}}</h1>
|
||||||
|
{{if len .Hyphae | not}}
|
||||||
|
<p>{{block "empty cat" .}}This category is empty{{end}}</p>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .GivenPermissionToModify}}
|
||||||
|
<h2>{{block "add to category title" .}}Add a hypha to the category{{end}}</h2>
|
||||||
|
<form method="POST" action="/add-to-category" class="add-to-category">
|
||||||
|
<input type="text" name="hypha" id="_hypha-name"
|
||||||
|
placeholder="{{block `hypha name` .}}Hypha name{{end}}">
|
||||||
|
<input type="hidden" name="cat" value="{{.CatName}}">
|
||||||
|
<input type="hidden" name="redirect-to" value="/category/{{.CatName}}">
|
||||||
|
<input type="submit" class="btn" value="{{block `add` .}}Add{{end}}">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{if len .Hyphae}}
|
||||||
|
<h2>{{block "remove hyphae" .}}Remove hyphae from the category{{end}}</h2>
|
||||||
|
<form method="POST" action="/remove-from-category" class="multi-remove-from-category">
|
||||||
|
<ol>
|
||||||
|
{{range .Hyphae}}
|
||||||
|
<li>
|
||||||
|
<input type="checkbox" name="_{{.}}" id="_{{.}}">
|
||||||
|
<label for="_{{.}}"><a href="/hypha/{{.}}">{{beautifulName .}}</a></label>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ol>
|
||||||
|
<input type="hidden" name="cat" value="{{.CatName}}">
|
||||||
|
<input type="hidden" name="redirect-to" value="/edit-category/{{.CatName}}">
|
||||||
|
<input type="submit" class="btn" value="{{block `remove` .}}Remove{{end}}">
|
||||||
|
</form>
|
||||||
|
{{end}}{{end}}
|
||||||
|
</main>
|
||||||
|
{{end}}
|
||||||
@ -1,13 +1,13 @@
|
|||||||
{{define "category list"}}Category list{{end}}
|
{{define "category list"}}Category list{{end}}
|
||||||
{{define "title"}}{{template "category list"}}{{end}}
|
{{define "title"}}{{template "category list"}}{{end}}
|
||||||
{{define "body"}}
|
{{define "body"}}
|
||||||
<main class="main-width mv-tags">
|
<main class="main-width">
|
||||||
<h1 class="p-name">{{template "title"}}</h1>
|
<h1>{{template "title"}}</h1>
|
||||||
{{if len .Categories}}
|
{{if len .Categories}}
|
||||||
<ol>
|
<ol>
|
||||||
{{range .Categories}}
|
{{range .Categories}}
|
||||||
<li class="mv-tag">
|
<li>
|
||||||
<a class="wikilink u-url p-name" href="/category/{{.}}">{{beautifulName .}}</a>
|
<a class="wikilink" href="/category/{{.}}">{{beautifulName .}}</a>
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ol>
|
</ol>
|
||||||
107
categories/views.go
Normal file
107
categories/views.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package categories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/viewutil"
|
||||||
|
"log"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ruTranslation = `
|
||||||
|
{{define "empty cat"}}Эта категория пуста.{{end}}
|
||||||
|
{{define "cat"}}Категория{{end}}
|
||||||
|
{{define "hypha name"}}Название гифы{{end}}
|
||||||
|
{{define "categories"}}Категории{{end}}
|
||||||
|
{{define "placeholder"}}Название категории...{{end}}
|
||||||
|
{{define "remove from category title"}}Убрать гифу из этой категории{{end}}
|
||||||
|
{{define "add to category title"}}Добавить гифу в эту категорию{{end}}
|
||||||
|
{{define "category list"}}Список категорий{{end}}
|
||||||
|
{{define "no categories"}}В этой вики нет категорий.{{end}}
|
||||||
|
{{define "category x"}}Категория {{. | beautifulName}}{{end}}
|
||||||
|
|
||||||
|
{{define "edit category x"}}Редактирование категории {{beautifulName .}}{{end}}
|
||||||
|
{{define "edit category heading"}}Редактирование категории <a href="/category/{{.}}">{{beautifulName .}}</a>{{end}}
|
||||||
|
{{define "add"}}Добавить{{end}}
|
||||||
|
{{define "remove hyphae"}}Убрать гифы из этой категории{{end}}
|
||||||
|
{{define "remove"}}Убрать{{end}}
|
||||||
|
{{define "edit"}}Редактировать{{end}}
|
||||||
|
`
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed *.html
|
||||||
|
fs embed.FS
|
||||||
|
viewListChain, viewPageChain, viewCardChain, viewEditChain viewutil.Chain
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepareViews() {
|
||||||
|
viewCardChain = viewutil.CopyEnRuWith(fs, "view_card.html", ruTranslation)
|
||||||
|
viewListChain = viewutil.CopyEnRuWith(fs, "view_list.html", ruTranslation)
|
||||||
|
viewPageChain = viewutil.CopyEnRuWith(fs, "view_page.html", ruTranslation)
|
||||||
|
viewEditChain = viewutil.CopyEnRuWith(fs, "view_edit.html", ruTranslation)
|
||||||
|
}
|
||||||
|
|
||||||
|
type cardData struct {
|
||||||
|
HyphaName string
|
||||||
|
Categories []string
|
||||||
|
GivenPermissionToModify bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// CategoryCard is the little sidebar that is shown nearby the hypha view.
|
||||||
|
func CategoryCard(meta viewutil.Meta, hyphaName string) string {
|
||||||
|
var buf strings.Builder
|
||||||
|
err := viewCardChain.Get(meta).ExecuteTemplate(&buf, "category card", cardData{
|
||||||
|
HyphaName: hyphaName,
|
||||||
|
Categories: categoriesWithHypha(hyphaName),
|
||||||
|
GivenPermissionToModify: meta.U.CanProceed("add-to-category"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type catData struct {
|
||||||
|
*viewutil.BaseData
|
||||||
|
CatName string
|
||||||
|
Hyphae []string
|
||||||
|
GivenPermissionToModify bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func categoryEdit(meta viewutil.Meta, catName string) {
|
||||||
|
viewutil.ExecutePage(meta, viewEditChain, catData{
|
||||||
|
BaseData: &viewutil.BaseData{
|
||||||
|
Addr: "/edit-category/" + catName,
|
||||||
|
},
|
||||||
|
CatName: catName,
|
||||||
|
Hyphae: hyphaeInCategory(catName),
|
||||||
|
GivenPermissionToModify: meta.U.CanProceed("add-to-category"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func categoryPage(meta viewutil.Meta, catName string) {
|
||||||
|
viewutil.ExecutePage(meta, viewPageChain, catData{
|
||||||
|
BaseData: &viewutil.BaseData{
|
||||||
|
Addr: "/category/" + catName,
|
||||||
|
},
|
||||||
|
CatName: catName,
|
||||||
|
Hyphae: hyphaeInCategory(catName),
|
||||||
|
GivenPermissionToModify: meta.U.CanProceed("add-to-category"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type listData struct {
|
||||||
|
*viewutil.BaseData
|
||||||
|
Categories []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func categoryList(meta viewutil.Meta) {
|
||||||
|
cats := listOfCategories()
|
||||||
|
sort.Strings(cats)
|
||||||
|
viewutil.ExecutePage(meta, viewListChain, listData{
|
||||||
|
BaseData: &viewutil.BaseData{
|
||||||
|
Addr: "/category",
|
||||||
|
},
|
||||||
|
Categories: cats,
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -19,6 +19,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
WikiName string
|
WikiName string
|
||||||
NaviTitleIcon string
|
NaviTitleIcon string
|
||||||
|
UseSiblingHyphaeSidebar bool
|
||||||
|
|
||||||
HomeHypha string
|
HomeHypha string
|
||||||
UserHypha string
|
UserHypha string
|
||||||
@ -54,6 +55,7 @@ var WikiDir string
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
WikiName string `comment:"This name appears in the header and on various pages."`
|
WikiName string `comment:"This name appears in the header and on various pages."`
|
||||||
NaviTitleIcon string `comment:"This icon is used in the breadcrumbs bar."`
|
NaviTitleIcon string `comment:"This icon is used in the breadcrumbs bar."`
|
||||||
|
UseSiblingHyphaeSidebar bool `comment:"You are discouraged from using the sibling hyphae sidebar on new wikis. Enable it on old wikis that depend on it heavily."`
|
||||||
Hyphae
|
Hyphae
|
||||||
Network
|
Network
|
||||||
Authorization
|
Authorization
|
||||||
@ -109,22 +111,23 @@ type Telegram struct {
|
|||||||
// configuration. Call it sometime during the initialization.
|
// configuration. Call it sometime during the initialization.
|
||||||
func ReadConfigFile(path string) error {
|
func ReadConfigFile(path string) error {
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
WikiName: "Lyxi's Vault",
|
WikiName: "Mycorrhiza Wiki",
|
||||||
NaviTitleIcon: "🦇",
|
NaviTitleIcon: "🍄",
|
||||||
|
UseSiblingHyphaeSidebar: false,
|
||||||
Hyphae: Hyphae{
|
Hyphae: Hyphae{
|
||||||
HomeHypha: "home",
|
HomeHypha: "home",
|
||||||
UserHypha: "u",
|
UserHypha: "u",
|
||||||
HeaderLinksHypha: "u/alyxbatte/header",
|
HeaderLinksHypha: "",
|
||||||
RedirectionCategory: "redirection",
|
RedirectionCategory: "redirection",
|
||||||
},
|
},
|
||||||
Network: Network{
|
Network: Network{
|
||||||
ListenAddr: "0.0.0.0:1737",
|
ListenAddr: "127.0.0.1:1737",
|
||||||
URL: "",
|
URL: "",
|
||||||
},
|
},
|
||||||
Authorization: Authorization{
|
Authorization: Authorization{
|
||||||
UseAuth: true,
|
UseAuth: false,
|
||||||
AllowRegistration: false,
|
AllowRegistration: false,
|
||||||
RegistrationLimit: 1,
|
RegistrationLimit: 0,
|
||||||
Locked: false,
|
Locked: false,
|
||||||
UseWhiteList: false,
|
UseWhiteList: false,
|
||||||
WhiteList: []string{},
|
WhiteList: []string{},
|
||||||
@ -171,6 +174,7 @@ func ReadConfigFile(path string) error {
|
|||||||
// Map the struct to the global variables
|
// Map the struct to the global variables
|
||||||
WikiName = cfg.WikiName
|
WikiName = cfg.WikiName
|
||||||
NaviTitleIcon = cfg.NaviTitleIcon
|
NaviTitleIcon = cfg.NaviTitleIcon
|
||||||
|
UseSiblingHyphaeSidebar = cfg.UseSiblingHyphaeSidebar
|
||||||
HomeHypha = cfg.HomeHypha
|
HomeHypha = cfg.HomeHypha
|
||||||
UserHypha = cfg.UserHypha
|
UserHypha = cfg.UserHypha
|
||||||
HeaderLinksHypha = cfg.HeaderLinksHypha
|
HeaderLinksHypha = cfg.HeaderLinksHypha
|
||||||
@ -2,12 +2,10 @@
|
|||||||
package files
|
package files
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/cfg"
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
"github.com/bouncepaw/mycorrhiza/web/static"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var paths struct {
|
var paths struct {
|
||||||
@ -25,7 +23,7 @@ var paths struct {
|
|||||||
// A separate function is needed to easily know where a general storage path is
|
// A separate function is needed to easily know where a general storage path is
|
||||||
// needed rather than a concrete Git or the whole wiki storage path, so that we
|
// needed rather than a concrete Git or the whole wiki storage path, so that we
|
||||||
// could easily refactor things later if we'll ever support different storages.
|
// could easily refactor things later if we'll ever support different storages.
|
||||||
func HyphaeDir() string { return filepath.ToSlash(paths.gitRepo) }
|
func HyphaeDir() string { return paths.gitRepo }
|
||||||
|
|
||||||
// GitRepo returns the path to the Git repository of the wiki.
|
// GitRepo returns the path to the Git repository of the wiki.
|
||||||
func GitRepo() string { return paths.gitRepo }
|
func GitRepo() string { return paths.gitRepo }
|
||||||
@ -53,10 +51,6 @@ func InterwikiJSON() string { return paths.interwikiJSON }
|
|||||||
// PrepareWikiRoot ensures all needed directories and files exist and have
|
// PrepareWikiRoot ensures all needed directories and files exist and have
|
||||||
// correct permissions.
|
// correct permissions.
|
||||||
func PrepareWikiRoot() error {
|
func PrepareWikiRoot() error {
|
||||||
isFirstInit := false
|
|
||||||
if _, err := os.Stat(cfg.WikiDir); err != nil && os.IsNotExist(err) {
|
|
||||||
isFirstInit = true
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(cfg.WikiDir, os.ModeDir|0777); err != nil {
|
if err := os.MkdirAll(cfg.WikiDir, os.ModeDir|0777); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -83,41 +77,5 @@ func PrepareWikiRoot() error {
|
|||||||
paths.categoriesJSON = filepath.Join(cfg.WikiDir, "categories.json")
|
paths.categoriesJSON = filepath.Join(cfg.WikiDir, "categories.json")
|
||||||
paths.interwikiJSON = FileInRoot("interwiki.json")
|
paths.interwikiJSON = FileInRoot("interwiki.json")
|
||||||
|
|
||||||
// Are we initializing the wiki for the first time?
|
|
||||||
if isFirstInit {
|
|
||||||
err := firstTimeInit()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// firstTimeInit takes care of any tasks that only need to happen the first time the wiki is initialized
|
|
||||||
func firstTimeInit() error {
|
|
||||||
static.InitFS(StaticFiles())
|
|
||||||
|
|
||||||
defaultFavicon, err := static.FS.Open("icon/mushroom.png")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer defaultFavicon.Close()
|
|
||||||
|
|
||||||
outputFileName := filepath.Join(cfg.WikiDir, "static", "favicon.ico")
|
|
||||||
|
|
||||||
outputFile, err := os.Create(outputFileName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer outputFile.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(outputFile, defaultFavicon)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
98
flag.go
98
flag.go
@ -1,21 +1,18 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"errors"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/cfg"
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/files"
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/user"
|
"github.com/bouncepaw/mycorrhiza/user"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/version"
|
|
||||||
|
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -23,7 +20,7 @@ import (
|
|||||||
|
|
||||||
// printHelp prints the help message.
|
// printHelp prints the help message.
|
||||||
func printHelp() {
|
func printHelp() {
|
||||||
_, _ = fmt.Fprintf(
|
fmt.Fprintf(
|
||||||
flag.CommandLine.Output(),
|
flag.CommandLine.Output(),
|
||||||
"Usage: %s WIKI_PATH\n",
|
"Usage: %s WIKI_PATH\n",
|
||||||
os.Args[0],
|
os.Args[0],
|
||||||
@ -32,94 +29,65 @@ func printHelp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parseCliArgs parses CLI options and sets several important global variables. Call it early.
|
// parseCliArgs parses CLI options and sets several important global variables. Call it early.
|
||||||
func parseCliArgs() error {
|
func parseCliArgs() {
|
||||||
var createAdminName string
|
var createAdminName string
|
||||||
var versionFlag bool
|
|
||||||
|
|
||||||
flag.StringVar(&cfg.ListenAddr, "listen-addr", "", "Address to listen on. For example, 127.0.0.1:1737 or /run/mycorrhiza.sock.")
|
flag.StringVar(&cfg.ListenAddr, "listen-addr", "", "Address to listen on. For example, 127.0.0.1:1737 or /run/mycorrhiza.sock.")
|
||||||
flag.StringVar(&createAdminName, "create-admin", "", "Create a new admin. The password will be prompted in the terminal.")
|
flag.StringVar(&createAdminName, "create-admin", "", "Create a new admin. The password will be prompted in the terminal.")
|
||||||
flag.BoolVar(&versionFlag, "version", false, "Print version information and exit.")
|
|
||||||
flag.Usage = printHelp
|
flag.Usage = printHelp
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if versionFlag {
|
|
||||||
slog.Info("Running Mycorrhiza Wiki", "version", version.Long)
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
args := flag.Args()
|
args := flag.Args()
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
slog.Error("Pass a wiki directory")
|
log.Fatal("error: pass a wiki directory")
|
||||||
return errors.New("wiki directory not passed")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wikiDir, err := filepath.Abs(args[0])
|
wikiDir, err := filepath.Abs(args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to take absolute filepath of wiki directory",
|
log.Fatal(err)
|
||||||
"path", args[0], "err", err)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.WikiDir = wikiDir
|
cfg.WikiDir = wikiDir
|
||||||
|
|
||||||
if createAdminName != "" {
|
if createAdminName != "" {
|
||||||
if err := createAdminCommand(createAdminName); err != nil {
|
createAdminCommand(createAdminName)
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createAdminCommand(name string) error {
|
func createAdminCommand(name string) {
|
||||||
|
wr := log.Writer()
|
||||||
|
log.SetFlags(0)
|
||||||
|
|
||||||
if err := files.PrepareWikiRoot(); err != nil {
|
if err := files.PrepareWikiRoot(); err != nil {
|
||||||
slog.Error("Failed to prepare wiki root", "err", err)
|
log.Fatal("error: ", err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
cfg.UseAuth = true
|
cfg.UseAuth = true
|
||||||
cfg.AllowRegistration = true
|
cfg.AllowRegistration = true
|
||||||
|
|
||||||
|
log.SetOutput(io.Discard)
|
||||||
user.InitUserDatabase()
|
user.InitUserDatabase()
|
||||||
|
log.SetOutput(wr)
|
||||||
|
|
||||||
password, err := askPass("Password")
|
handle /*rug*/ := syscall.Stdin
|
||||||
|
if !term.IsTerminal(handle) {
|
||||||
|
log.Fatal("error: not a terminal")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("Password: ")
|
||||||
|
passwordBytes, err := term.ReadPassword(handle)
|
||||||
|
fmt.Print("\n")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to prompt password", "err", err)
|
log.Fatal("error: ", err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
if err := user.Register(name, password, "admin", "local", true); err != nil {
|
|
||||||
slog.Error("Failed to register admin", "err", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func askPass(prompt string) (string, error) {
|
password := string(passwordBytes)
|
||||||
var password []byte
|
|
||||||
var err error
|
log.SetOutput(io.Discard)
|
||||||
fd := int(os.Stdin.Fd())
|
err = user.Register(name, password, "admin", "local", true)
|
||||||
|
log.SetOutput(wr)
|
||||||
|
|
||||||
if term.IsTerminal(fd) {
|
|
||||||
fmt.Printf("%s: ", prompt)
|
|
||||||
password, err = term.ReadPassword(int(os.Stdin.Fd()))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
log.Fatal("error: ", err)
|
||||||
}
|
}
|
||||||
fmt.Println()
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(os.Stderr, "Warning: Reading password from stdin.\n")
|
|
||||||
// TODO: the buffering messes up repeated calls to readPassword
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
|
||||||
if !scanner.Scan() {
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return "", io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
password = scanner.Bytes()
|
|
||||||
|
|
||||||
if len(password) == 0 {
|
|
||||||
return "", fmt.Errorf("zero length password")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(password), nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
28
go.mod
28
go.mod
@ -1,22 +1,30 @@
|
|||||||
module github.com/bouncepaw/mycorrhiza
|
module github.com/bouncepaw/mycorrhiza
|
||||||
|
|
||||||
go 1.21
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.sr.ht/~bouncepaw/mycomarkup/v5 v5.6.0
|
github.com/bouncepaw/mycomarkup/v5 v5.1.1
|
||||||
github.com/go-ini/ini v1.67.0
|
github.com/go-ini/ini v1.63.2
|
||||||
github.com/gorilla/feeds v1.2.0
|
github.com/gorilla/feeds v1.1.1
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.0
|
||||||
golang.org/x/crypto v0.31.0
|
github.com/valyala/quicktemplate v1.7.0
|
||||||
golang.org/x/term v0.27.0
|
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa
|
||||||
golang.org/x/text v0.21.0
|
golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||||
|
golang.org/x/text v0.3.7
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/kr/pretty v0.2.1 // indirect
|
||||||
github.com/stretchr/testify v1.7.0 // indirect
|
github.com/stretchr/testify v1.7.0 // indirect
|
||||||
golang.org/x/sys v0.28.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20211109184856-51b60fd695b3 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
// Use this trick to test local Mycomarkup changes, replace the path with yours,
|
// Use this trick to test local Mycomarkup changes, replace the path with yours,
|
||||||
// but do not commit the change to the path:
|
// but do not commit the change to the path:
|
||||||
// replace git.sr.ht/~bouncepaw/mycomarkup/v5 v5.6.0 => "/Users/bouncepaw/src/mycomarkup"
|
// replace github.com/bouncepaw/mycomarkup/v5 v5.1.1 => "/Users/bouncepaw/GolandProjects/mycomarkup"
|
||||||
|
|
||||||
|
// Use this utility every time Mycomarkup gets a major update:
|
||||||
|
// https://github.com/marwan-at-work/mod
|
||||||
|
// Or maybe just ⌘⇧R every time, the utility is kinda weird.
|
||||||
|
|||||||
66
go.sum
66
go.sum
@ -1,32 +1,54 @@
|
|||||||
git.sr.ht/~bouncepaw/mycomarkup/v5 v5.6.0 h1:zAZwMF+6x8U/nunpqPRVYoDiqVUMBHI04PG8GsDrFOk=
|
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||||
git.sr.ht/~bouncepaw/mycomarkup/v5 v5.6.0/go.mod h1:TCzFBqW11En4EjLfcQtJu8C/Ro7FIFR8vZ+nM9f6Q28=
|
github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
|
github.com/bouncepaw/mycomarkup/v5 v5.1.1 h1:ytg+cnDtOFu1KDrEkxS7VNUVDp1L9RXtVe2KPxyElsg=
|
||||||
|
github.com/bouncepaw/mycomarkup/v5 v5.1.1/go.mod h1:jyB/vxKe3X8SsN7FjjPf24IZwFM/H1C4LNvQ5UyXwjU=
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
github.com/go-ini/ini v1.63.2 h1:kwN3umicd2HF3Tgvap4um1ZG52/WyKT9GGdPx0CJk6Y=
|
||||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
github.com/go-ini/ini v1.63.2/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc=
|
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
|
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
|
||||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
|
||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
github.com/valyala/quicktemplate v1.7.0 h1:LUPTJmlVcb46OOUY3IeD9DojFpAVbsG+5WFTcjMJzCM=
|
||||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8=
|
||||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4=
|
||||||
|
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd h1:zVFyTKZN/Q7mNRWSs1GOYnHM9NiFSJ54YVRsD0rNWT4=
|
||||||
|
golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211109184856-51b60fd695b3 h1:T6tyxxvHMj2L1R2kZg0uNMpS8ZhB9lRa9XRGTCSA65w=
|
||||||
|
golang.org/x/sys v0.0.0-20211109184856-51b60fd695b3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
= Help
|
= Help
|
||||||
|
|
||||||
This is documentation for Mycorrhiza Wiki 1.15.1. Choose a topic from the list.
|
This is documentation for **Mycorrhiza Wiki** 1.11.
|
||||||
|
|
||||||
The documentation is incomplete. If you want to contribute to the documentation, open a pull request or an issue on [[https://github.com/bouncepaw/mycorrhiza | GitHub]] or [[https://lists.sr.ht/~bouncepaw/mycorrhiza-devel | send a patch]].
|
//Choose a topic from the list.//
|
||||||
|
|
||||||
|
Currently, the documentation does not cover everything there is to cover, but it will probably change in the future.
|
||||||
|
|
||||||
|
If you want to contribute to the documentation, open a pull request or an issue on [[https://github.com/bouncepaw/mycorrhiza | GitHub]].
|
||||||
|
|
||||||
=> https://mycorrhiza.wiki | Official wiki
|
=> https://mycorrhiza.wiki | Official wiki
|
||||||
=> https://floss.social/@mycorrhiza | Fediverse
|
|
||||||
|
|||||||
@ -42,6 +42,7 @@ AllowRegistration = true
|
|||||||
=== Root section
|
=== Root section
|
||||||
* `WikiName`: //string//. The name your wiki has. It is shown in the header by default and in other places. **Default:** `Mycorrhiza Wiki`.
|
* `WikiName`: //string//. The name your wiki has. It is shown in the header by default and in other places. **Default:** `Mycorrhiza Wiki`.
|
||||||
* `NaviTitleIcon`: //string//. The icon shown before the colon in the navigational titles above each hypha. You may want to use an emoji or HTML here. **Default:** `🍄` (mushroom emoji).
|
* `NaviTitleIcon`: //string//. The icon shown before the colon in the navigational titles above each hypha. You may want to use an emoji or HTML here. **Default:** `🍄` (mushroom emoji).
|
||||||
|
* `UseSiblingHyphaeSidebar`: //boolean//. Whether to show the [[/help/en/sibling_hyphae_section | sibling hyphae sidebar]]. You are discouraged from using the sibling hyphae sidebar on new wikis. Enable it on old wikis that depend on it heavily. **Default:** `false`.
|
||||||
|
|
||||||
=== [Hyphae]
|
=== [Hyphae]
|
||||||
* `HomeHypha`: //string//. The name your home hypha has. **Default:** `home`.
|
* `HomeHypha`: //string//. The name your home hypha has. **Default:** `home`.
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
= File structure
|
|
||||||
//This article is intended for wiki administrators.//
|
|
||||||
|
|
||||||
Every Mycorrhiza wiki is stored in one directory. This document explaints the structure of this directory, what files can be there, and what you should do with them.
|
|
||||||
|
|
||||||
You can edit all of the files manually, if you want, just do your best to not break anything.
|
|
||||||
|
|
||||||
* `config.ini` is the [[/help/en/config_file | configuration file]]. It has comments in it, feel free to edit it.
|
|
||||||
* `wiki.git/` is the Git repository of the wiki, it has all hyphae in it. You can edit it directly, but do not forget to make Git commits with your changes and [[/reindex]] you wiki afterwards.
|
|
||||||
* `static` holds static data. You can access data there from your wiki with addresses like `/static/image.png`.
|
|
||||||
** `static/favicon.ico` is your wiki's favicon, accessed at [[/favicon.ico]] by browsers.
|
|
||||||
** `static/default.css` redefines the engine's default style, if exists. You probably don't need to use it.
|
|
||||||
** `static/custom.css` is loaded after the main style. If you want to make visual changes to your wiki, this is probably where you should do that.
|
|
||||||
** `static/robots.txt` redefines default `robots.txt` file.
|
|
||||||
* `categories.json` contains the information about all categories in your wiki.
|
|
||||||
* `users.json` stores users' information. The passwords are not stored, only their hashes are, this is safe. Their tokens are stored in `cache/tokens.json`.
|
|
||||||
* `interwiki.json` holds the interwiki configuration.
|
|
||||||
* `cache/` holds cached data. If you back up your wiki, you can omit this directory.
|
|
||||||
** `cache/tokens.json` holds users' tokens. By deleting specific tokens, you can log out users remotely.
|
|
||||||
* Mycomarkup migration markers are hidden files prefixed with `.mycomarkup-`. You should probably not touch them.
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
= Interwiki
|
= Interwiki
|
||||||
**Interwiki** is a means of bringing wikis together, making a federation of them. In Mycorrhiza, one aspect of interwiki is supported: the interwiki links, or the **interlinks**. Most other wiki systems support them too. Interwiki links are shown in green.
|
**Interwiki** is a means of bringing wikis together, making a federation of them. In Mycorrhiza, one aspect of interwiki is supported: the interwiki links, or the **interlinks**. Most other wiki systems support it too. Interwiki links are shown in green.
|
||||||
|
|
||||||
In Mycomarkup, you can address a different wiki by prefixing the link target with a name and a `>` character. For example:
|
In Mycomarkup, you can address a different wiki by prefixing the link target with a name and a `>` character. For example:
|
||||||
```myco
|
```myco
|
||||||
@ -60,7 +60,7 @@ Entries have the following fields:
|
|||||||
|
|
||||||
For generic sites, you will have to think of something by yourself. If you do not set it, it will default to `https\:/\/example.org/{NAME}`.
|
For generic sites, you will have to think of something by yourself. If you do not set it, it will default to `https\:/\/example.org/{NAME}`.
|
||||||
|
|
||||||
`{NAME}` is substituted. For example, from link `[\[Melanocarpa\>uxn]]`, `{NAME}` is replaced with `uxn`.
|
`{NAME}` is substituted. For example, from link `[[Melanocarpa>uxn]]`, `{NAME}` is replaced with `uxn`.
|
||||||
}
|
}
|
||||||
|
|
||||||
You can also change `interwiki.json` directly. Reload the wiki after editing it.
|
You can also change `interwiki.json` directly. Reload the wiki after editing it.
|
||||||
@ -23,8 +23,5 @@ https://mycorrhiza.wiki/binary/release/1.3/lock_screenshot
|
|||||||
|
|
||||||
If they log in, they will be able to navigate the wiki. They are not able to register, though. [[/help/en/telegram | Telegram]] authorization is supported.
|
If they log in, they will be able to navigate the wiki. They are not able to register, though. [[/help/en/telegram | Telegram]] authorization is supported.
|
||||||
|
|
||||||
== Readers
|
|
||||||
You might want to add some users to the `reader` group. Readers can log in even to locked wikis, but they cannot edit.
|
|
||||||
|
|
||||||
== See also
|
== See also
|
||||||
=> /help/en/whitelist | Whitelist
|
=> /help/en/whitelist | Whitelist
|
||||||
@ -6,7 +6,7 @@ You can upload any media file, but only those listed below will be displayed on
|
|||||||
|
|
||||||
* **Images:** jpg, gif, png, webp, svg, ico
|
* **Images:** jpg, gif, png, webp, svg, ico
|
||||||
* **Video:** ogg, webm, mp4
|
* **Video:** ogg, webm, mp4
|
||||||
* **Audio:** ogg, webm, mp3, flac, wav
|
* **Audio:** ogg, webm, mp3
|
||||||
|
|
||||||
== How to upload media?
|
== How to upload media?
|
||||||
For non-existent hyphae, upload a file in the //Upload media// section.
|
For non-existent hyphae, upload a file in the //Upload media// section.
|
||||||
|
|||||||
@ -86,7 +86,7 @@ Links to [[hypha | a Hypha]] and [[https://example.org | some website]].
|
|||||||
```}
|
```}
|
||||||
* Links to [[hypha | a Hypha]] and [[https://example.org | some website]].
|
* Links to [[hypha | a Hypha]] and [[https://example.org | some website]].
|
||||||
|
|
||||||
Since hypha names are case-insensitive, these links are basically the same: `[\[hypha]]`, `[\[Hypha]]`, `[\[HYPHA]]`.
|
Since hypha names are case-insensitive, these links are basically the same: `[[hypha]]`, `[[Hypha]]`, `[[HYPHA]]`.
|
||||||
|
|
||||||
=== Rocket link
|
=== Rocket link
|
||||||
**Rocket links** are special links. They take up a whole line. They are not consistent with usual inline links. They were taken from [[https://gemini.circumlunar.space/docs/gemtext.gmi | gemtext]].
|
**Rocket links** are special links. They take up a whole line. They are not consistent with usual inline links. They were taken from [[https://gemini.circumlunar.space/docs/gemtext.gmi | gemtext]].
|
||||||
@ -163,22 +163,22 @@ You can write a description for the image and specify its size.
|
|||||||
|
|
||||||
* {```
|
* {```
|
||||||
img {
|
img {
|
||||||
https://bouncepaw.com/mushroom.jpg
|
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg
|
||||||
https://bouncepaw.com/mushroom.jpg {
|
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg {
|
||||||
Description //here//
|
Description //here//
|
||||||
}
|
}
|
||||||
https://bouncepaw.com/mushroom.jpg | 100 { Size }
|
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg | 100 { Size }
|
||||||
https://bouncepaw.com/mushroom.jpg | 50*50
|
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg | 50*50
|
||||||
}
|
}
|
||||||
```}
|
```}
|
||||||
* {
|
* {
|
||||||
img {
|
img {
|
||||||
https://bouncepaw.com/mushroom.jpg
|
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg
|
||||||
https://bouncepaw.com/mushroom.jpg {
|
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg {
|
||||||
Description //here//
|
Description //here//
|
||||||
}
|
}
|
||||||
https://bouncepaw.com/mushroom.jpg | 100 { Size }
|
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg | 100 { Size }
|
||||||
https://bouncepaw.com/mushroom.jpg | 50*50 { Square }
|
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg | 50*50 { Square }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,25 +205,25 @@ Specify the layout after `img` and before `{`. If you do not write any of them,
|
|||||||
|
|
||||||
```
|
```
|
||||||
img grid {
|
img grid {
|
||||||
https://bouncepaw.com/mushroom.jpg
|
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg
|
||||||
https://bouncepaw.com/mushroom.jpg
|
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg
|
||||||
}
|
}
|
||||||
|
|
||||||
img side {
|
img side {
|
||||||
https://bouncepaw.com/mushroom.jpg | 200
|
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg | 200
|
||||||
https://bouncepaw.com/mushroom.jpg | 200
|
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg | 200
|
||||||
}
|
}
|
||||||
|
|
||||||
This text is wrapped.
|
This text is wrapped.
|
||||||
```
|
```
|
||||||
img grid {
|
img grid {
|
||||||
https://bouncepaw.com/mushroom.jpg
|
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg
|
||||||
https://bouncepaw.com/mushroom.jpg
|
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg
|
||||||
}
|
}
|
||||||
|
|
||||||
img side {
|
img side {
|
||||||
https://bouncepaw.com/mushroom.jpg | 200
|
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg | 200
|
||||||
https://bouncepaw.com/mushroom.jpg | 200
|
https://upload.wikimedia.org/wikipedia/commons/4/48/Timbre_ciuperci_otravitoare.jpg | 200
|
||||||
}
|
}
|
||||||
|
|
||||||
This text is wrapped.
|
This text is wrapped.
|
||||||
@ -425,4 +425,4 @@ This is an actual transclusion of a hypha below. It will fail if your wiki does
|
|||||||
Recursive transclusion is also supported but it is limited to three iterations.
|
Recursive transclusion is also supported but it is limited to three iterations.
|
||||||
|
|
||||||
== See also
|
== See also
|
||||||
=> https://mycorrhiza.wiki/hypha/why_mycomarkup | Why it was created
|
=> https://mycorrhiza.wiki/hypha/essay/why_mycomarkup | Why it was created
|
||||||
|
|||||||
13
help/en/sibling_hyphae_section.myco
Normal file
13
help/en/sibling_hyphae_section.myco
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
= Sibling hyphae section
|
||||||
|
On the right (or below on smaller devices) of hypha pages there is a special section that lists **sibling hyphae**.
|
||||||
|
|
||||||
|
> **Sibling hyphae** are hyphae that are subhyphae of the same hypha. For example, //Fruit/Apple// and //Fruit/Pear// are sibling hyphae to each other.
|
||||||
|
|
||||||
|
The sibling hyphae are listed alphabetically. The name of the hypha you are currently viewing is also part of the list. The rest are links that lead you to the hyphae.
|
||||||
|
|
||||||
|
Sometimes, there are numbers beside the links:
|
||||||
|
* **No number.** The hypha has no subhyphae.
|
||||||
|
* **One number.** The number indicates how many direct subhyphae it has.
|
||||||
|
* **Two numbers.** The first number is the number of direct subhyphae. The second number in parentheses is the number of indirect subhyphae.
|
||||||
|
|
||||||
|
For hypha //Fruit//, hyphae //Fruit/Apple// and //Fruit/Pear// would be direct subhyphae, and hyphae //Fruit/Apple/Red// and //Fruit/Apple/Green// would be indirect subhyphae.
|
||||||
@ -1,4 +0,0 @@
|
|||||||
= Today links
|
|
||||||
Some people use Mycorrhiza to keep their diaries. They put each day's notes into separate hyphae, usually named in the ISO format, for example, `2024-06-01` for the 1st of June, 2024. This date format is especially handy because it aligns with the way Mycorrhiza sorts hyphae, so the days are in order.
|
|
||||||
|
|
||||||
Links [[/today]] and [[/edit-today]] are special links that redirect to or edit the “today” hypha. Put them on your home hypha or on [[/help/en/top_bar | the top bar]].
|
|
||||||
@ -9,7 +9,6 @@ import (
|
|||||||
var fs embed.FS
|
var fs embed.FS
|
||||||
|
|
||||||
// Get determines what help text you need and returns it. The path is a substring from URL, it follows this form:
|
// Get determines what help text you need and returns it. The path is a substring from URL, it follows this form:
|
||||||
//
|
|
||||||
// <language>/<topic>
|
// <language>/<topic>
|
||||||
func Get(path string) ([]byte, error) {
|
func Get(path string) ([]byte, error) {
|
||||||
if path == "" {
|
if path == "" {
|
||||||
|
|||||||
@ -11,39 +11,38 @@
|
|||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
<aside class="help-topics layout-card">
|
<aside class="help-topics layout-card">
|
||||||
<h2 class="layout-card__title">Help topics</h2>
|
<h2 class="layout-card__title">{{block "topics" .}}Help topics{{end}}</h2>
|
||||||
<ul class="help-topics__list">
|
<ul class="help-topics__list">
|
||||||
<li><a href="/help/en">Main</a></li>
|
<li><a href="/help/en">{{block "main" .}}Main{{end}}</a></li>
|
||||||
<li><a href="/help/en/hypha">Hypha</a>
|
<li><a href="/help/en/hypha">{{block "hypha" .}}Hypha{{end}}</a>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/help/en/media">Media</a></li>
|
<a href="/help/en/media">{{block "media" .}}Media{{end}}</a>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="/help/en/mycomarkup">Mycomarkup</a></li>
|
<li><a href="/help/en/mycomarkup">{{block "mycomarkup" .}}Mycomarkup{{end}}</a></li>
|
||||||
<li><a href="/help/en/category">Categories</a></li>
|
<li><a href="/help/en/category">{{block "category" .}}Categories{{end}}</a></li>
|
||||||
<li><a href="/help/en/rename">Renaming</a></li>
|
<li><a href="/help/en/rename">{{block "rename" .}}Renaming{{end}}</a></li>
|
||||||
<li>Interface
|
<li>{{block "interface" .}}Interface{{end}}
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/help/en/prevnext">Previous/next</a></li>
|
<li><a href="/help/en/prevnext">{{block "prevnext" .}}Previous/next{{end}}</a></li>
|
||||||
<li><a href="/help/en/top_bar">Top bar</a></li>
|
<li><a href="/help/en/top_bar">{{block "top_bar" .}}Top bar{{end}}</a></li>
|
||||||
|
<li><a href="/help/en/sibling_hyphae_section">{{block "sibling_hyphae" .}}Sibling hyphae{{end}}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Special pages
|
<li>{{block "special pages" .}}Special pages{{end}}
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/help/en/recent_changes">Recent changes</a></li>
|
<li><a href="/help/en/recent_changes">{{block "recent_changes" .}}Recent changes{{end}}</a></li>
|
||||||
<li><a href="/help/en/feeds">Feeds</a></li>
|
<li><a href="/help/en/feeds">{{block "feeds" .}}Feeds{{end}}</a></li>
|
||||||
<li><a href="/help/en/orphans">Orphaned hyphae</a></li>
|
<li><a href="/help/en/orphans">{{block "orphans" .}}Orphaned hyphae{{end}}</a></li>
|
||||||
<li><a href="/help/en/today">Today links</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Configuration (for administrators)
|
<li>{{block "configuration" .}}Configuration (for administrators){{end}}
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/help/en/config_file">Configuration file</a></li>
|
<li><a href="/help/en/config_file">{{block "config_file" .}}Configuration file{{end}}</a></li>
|
||||||
<li><a href="/help/en/lock">Lock</a></li>
|
<li><a href="/help/en/lock">{{block "lock" .}}Lock{{end}}</a></li>
|
||||||
<li><a href="/help/en/whitelist">Whitelist</a></li>
|
<li><a href="/help/en/whitelist">{{block "whitelist" .}}Whitelist{{end}}</a></li>
|
||||||
<li><a href="/help/en/telegram">Telegram authentication</a></li>
|
<li><a href="/help/en/telegram">{{block "telegram" .}}Telegram authentication{{end}}</a></li>
|
||||||
<li><a href="/help/en/interwiki">Interwiki</a></li>
|
<li><a href="/help/en/interwiki">{{block "interwiki" .}}Interwiki{{end}}</a></li>
|
||||||
<li><a href="/help/en/file_structure">File structure</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
14
help/web.go
14
help/web.go
@ -2,17 +2,15 @@ package help
|
|||||||
|
|
||||||
// stuff.go is used for meta stuff about the wiki or all hyphae at once.
|
// stuff.go is used for meta stuff about the wiki or all hyphae at once.
|
||||||
import (
|
import (
|
||||||
|
"github.com/bouncepaw/mycomarkup/v5"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/mycoopts"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/viewutil"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/mycoopts"
|
"github.com/bouncepaw/mycomarkup/v5/mycocontext"
|
||||||
"github.com/bouncepaw/mycorrhiza/web/viewutil"
|
|
||||||
|
|
||||||
"git.sr.ht/~bouncepaw/mycomarkup/v5"
|
|
||||||
"git.sr.ht/~bouncepaw/mycomarkup/v5/mycocontext"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -31,6 +29,7 @@ var (
|
|||||||
{{define "interface"}}Интерфейс{{end}}
|
{{define "interface"}}Интерфейс{{end}}
|
||||||
{{define "prevnext"}}Пред/след{{end}}
|
{{define "prevnext"}}Пред/след{{end}}
|
||||||
{{define "top_bar"}}Верхняя панель{{end}}
|
{{define "top_bar"}}Верхняя панель{{end}}
|
||||||
|
{{define "sibling_hyphae"}}Гифы-сиблинги{{end}}
|
||||||
{{define "rename"}}Переименовывание{{end}}
|
{{define "rename"}}Переименовывание{{end}}
|
||||||
{{define "special pages"}}Специальные страницы{{end}}
|
{{define "special pages"}}Специальные страницы{{end}}
|
||||||
{{define "recent_changes"}}Свежие правки{{end}}
|
{{define "recent_changes"}}Свежие правки{{end}}
|
||||||
@ -42,7 +41,6 @@ var (
|
|||||||
{{define "whitelist"}}Белый список{{end}}
|
{{define "whitelist"}}Белый список{{end}}
|
||||||
{{define "telegram"}}Вход через Телеграм{{end}}
|
{{define "telegram"}}Вход через Телеграм{{end}}
|
||||||
{{define "interwiki"}}Интервики{{end}}
|
{{define "interwiki"}}Интервики{{end}}
|
||||||
{{define "file structure"}}Файловая структура{{end}}
|
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/cfg"
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
|
|
||||||
"github.com/gorilla/feeds"
|
"github.com/gorilla/feeds"
|
||||||
)
|
)
|
||||||
@ -121,7 +121,6 @@ func (grp revisionGroup) feedItem(opts FeedOptions) feeds.Item {
|
|||||||
Created: grp[len(grp)-1].Time, // earliest revision
|
Created: grp[len(grp)-1].Time, // earliest revision
|
||||||
Updated: grp[0].Time, // latest revision
|
Updated: grp[0].Time, // latest revision
|
||||||
Link: &feeds.Link{Href: cfg.URL + grp[0].bestLink()},
|
Link: &feeds.Link{Href: cfg.URL + grp[0].bestLink()},
|
||||||
Content: grp.descriptionForFeed(opts.order),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,12 +4,12 @@ package history
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/files"
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
"github.com/bouncepaw/mycorrhiza/util"
|
"github.com/bouncepaw/mycorrhiza/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -21,14 +21,12 @@ var renameMsgPattern = regexp.MustCompile(`^Rename ‘(.*)’ to ‘.*’`)
|
|||||||
var gitEnv = []string{"GIT_COMMITTER_NAME=wikimind", "GIT_COMMITTER_EMAIL=wikimind@mycorrhiza"}
|
var gitEnv = []string{"GIT_COMMITTER_NAME=wikimind", "GIT_COMMITTER_EMAIL=wikimind@mycorrhiza"}
|
||||||
|
|
||||||
// Start finds git and initializes git credentials.
|
// Start finds git and initializes git credentials.
|
||||||
func Start() error {
|
func Start() {
|
||||||
path, err := exec.LookPath("git")
|
path, err := exec.LookPath("git")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Could not find the Git executable. Check your $PATH.")
|
log.Fatal("Could not find the git executable. Check your $PATH.")
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
gitpath = path
|
gitpath = path
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitGitRepo checks a Git repository and initializes it if necessary.
|
// InitGitRepo checks a Git repository and initializes it if necessary.
|
||||||
@ -46,7 +44,7 @@ func InitGitRepo() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !isGitRepo {
|
if !isGitRepo {
|
||||||
slog.Info("Initializing Git repo", "path", files.HyphaeDir())
|
log.Println("Initializing Git repo at", files.HyphaeDir())
|
||||||
gitsh("init")
|
gitsh("init")
|
||||||
gitsh("config", "core.quotePath", "false")
|
gitsh("config", "core.quotePath", "false")
|
||||||
}
|
}
|
||||||
@ -58,11 +56,11 @@ func gitsh(args ...string) (out bytes.Buffer, err error) {
|
|||||||
fmt.Printf("$ %v\n", args)
|
fmt.Printf("$ %v\n", args)
|
||||||
cmd := exec.Command(gitpath, args...)
|
cmd := exec.Command(gitpath, args...)
|
||||||
cmd.Dir = files.HyphaeDir()
|
cmd.Dir = files.HyphaeDir()
|
||||||
cmd.Env = append(cmd.Environ(), gitEnv...)
|
cmd.Env = gitEnv
|
||||||
|
|
||||||
b, err := cmd.CombinedOutput()
|
b, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Info("Git command failed", "err", err, "output", string(b))
|
log.Println("gitsh:", err)
|
||||||
}
|
}
|
||||||
return *bytes.NewBuffer(b), err
|
return *bytes.NewBuffer(b), err
|
||||||
}
|
}
|
||||||
@ -71,7 +69,7 @@ func gitsh(args ...string) (out bytes.Buffer, err error) {
|
|||||||
func silentGitsh(args ...string) (out bytes.Buffer, err error) {
|
func silentGitsh(args ...string) (out bytes.Buffer, err error) {
|
||||||
cmd := exec.Command(gitpath, args...)
|
cmd := exec.Command(gitpath, args...)
|
||||||
cmd.Dir = files.HyphaeDir()
|
cmd.Dir = files.HyphaeDir()
|
||||||
cmd.Env = append(cmd.Environ(), gitEnv...)
|
cmd.Env = gitEnv
|
||||||
|
|
||||||
b, err := cmd.CombinedOutput()
|
b, err := cmd.CombinedOutput()
|
||||||
return *bytes.NewBuffer(b), err
|
return *bytes.NewBuffer(b), err
|
||||||
@ -79,9 +77,7 @@ func silentGitsh(args ...string) (out bytes.Buffer, err error) {
|
|||||||
|
|
||||||
// Rename renames from `from` to `to` using `git mv`.
|
// Rename renames from `from` to `to` using `git mv`.
|
||||||
func Rename(from, to string) error {
|
func Rename(from, to string) error {
|
||||||
slog.Info("Renaming file with git mv",
|
log.Println(util.ShorterPath(from), util.ShorterPath(to))
|
||||||
"from", util.ShorterPath(from),
|
|
||||||
"to", util.ShorterPath(to))
|
|
||||||
_, err := gitsh("mv", "--force", from, to)
|
_, err := gitsh("mv", "--force", from, to)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,22 +3,20 @@ package histweb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
"log/slog"
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/history"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/util"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/viewutil"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/history"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/cfg"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/files"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/util"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/web/viewutil"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitHandlers(rtr *mux.Router) {
|
func InitHandlers(rtr *mux.Router) {
|
||||||
@ -41,7 +39,15 @@ func handlerPrimitiveDiff(w http.ResponseWriter, rq *http.Request) {
|
|||||||
util.PrepareRq(rq)
|
util.PrepareRq(rq)
|
||||||
shorterURL := strings.TrimPrefix(rq.URL.Path, "/primitive-diff/")
|
shorterURL := strings.TrimPrefix(rq.URL.Path, "/primitive-diff/")
|
||||||
revHash, slug, found := strings.Cut(shorterURL, "/")
|
revHash, slug, found := strings.Cut(shorterURL, "/")
|
||||||
if !found || !util.IsRevHash(revHash) || len(slug) < 1 {
|
if !found || len(revHash) < 7 || len(slug) < 1 {
|
||||||
|
http.Error(w, "403 bad request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
paddedRevHash := revHash
|
||||||
|
if len(paddedRevHash)%2 != 0 {
|
||||||
|
paddedRevHash = paddedRevHash[:len(paddedRevHash)-1]
|
||||||
|
}
|
||||||
|
if _, err := hex.DecodeString(paddedRevHash); err != nil {
|
||||||
http.Error(w, "403 bad request", http.StatusBadRequest)
|
http.Error(w, "403 bad request", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -83,9 +89,7 @@ func handlerHistory(w http.ResponseWriter, rq *http.Request) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
list = history.WithRevisions(hyphaName, revs)
|
list = history.WithRevisions(hyphaName, revs)
|
||||||
}
|
}
|
||||||
|
log.Println("Found", len(revs), "revisions for", hyphaName)
|
||||||
// TODO: extra log, not needed?
|
|
||||||
slog.Info("Found revisions", "hyphaName", hyphaName, "n", len(revs), "err", err)
|
|
||||||
|
|
||||||
historyView(viewutil.MetaFrom(w, rq), hyphaName, list)
|
historyView(viewutil.MetaFrom(w, rq), hyphaName, list)
|
||||||
}
|
}
|
||||||
@ -118,7 +122,7 @@ func handlerRecentChangesAtom(w http.ResponseWriter, rq *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handlerRecentChangesJSON(w http.ResponseWriter, rq *http.Request) {
|
func handlerRecentChangesJSON(w http.ResponseWriter, rq *http.Request) {
|
||||||
genericHandlerOfFeeds(w, rq, history.RecentChangesJSON, "JSON feed", "application/feed+json")
|
genericHandlerOfFeeds(w, rq, history.RecentChangesJSON, "JSON feed", "application/json")
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -130,7 +134,6 @@ var (
|
|||||||
|
|
||||||
{{define "diff for at title"}}Разница для {{beautifulName .HyphaName}} для {{.Hash}}{{end}}
|
{{define "diff for at title"}}Разница для {{beautifulName .HyphaName}} для {{.Hash}}{{end}}
|
||||||
{{define "diff for at heading"}}Разница для <a href="/hypha/{{.HyphaName}}">{{beautifulName .HyphaName}}</a> для {{.Hash}}{{end}}
|
{{define "diff for at heading"}}Разница для <a href="/hypha/{{.HyphaName}}">{{beautifulName .HyphaName}}</a> для {{.Hash}}{{end}}
|
||||||
{{define "no text diff available"}}Нет текстовой разницы.{{end}}
|
|
||||||
|
|
||||||
{{define "count pre"}}Отобразить{{end}}
|
{{define "count pre"}}Отобразить{{end}}
|
||||||
{{define "count post"}}свежих правок.{{end}}
|
{{define "count post"}}свежих правок.{{end}}
|
||||||
@ -164,49 +167,15 @@ type primitiveDiffData struct {
|
|||||||
*viewutil.BaseData
|
*viewutil.BaseData
|
||||||
HyphaName string
|
HyphaName string
|
||||||
Hash string
|
Hash string
|
||||||
Text template.HTML
|
Text string
|
||||||
}
|
}
|
||||||
|
|
||||||
func primitiveDiff(meta viewutil.Meta, h hyphae.Hypha, hash, text string) {
|
func primitiveDiff(meta viewutil.Meta, h hyphae.Hypha, hash, text string) {
|
||||||
hunks := history.SplitPrimitiveDiff(text)
|
|
||||||
if len(hunks) > 0 {
|
|
||||||
var buf strings.Builder
|
|
||||||
for _, hunk := range hunks {
|
|
||||||
lines := strings.Split(hunk, "\n")
|
|
||||||
buf.WriteString(`<pre class="codeblock">`)
|
|
||||||
for i, line := range lines {
|
|
||||||
line = strings.Trim(line, "\r")
|
|
||||||
var class string
|
|
||||||
if len(line) > 0 {
|
|
||||||
switch line[0] {
|
|
||||||
case '+':
|
|
||||||
class = "primitive-diff__addition"
|
|
||||||
case '-':
|
|
||||||
class = "primitive-diff__deletion"
|
|
||||||
case '@':
|
|
||||||
class = "primitive-diff__context"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if i > 0 {
|
|
||||||
buf.WriteString("\n")
|
|
||||||
}
|
|
||||||
line = template.HTMLEscapeString(line)
|
|
||||||
fmt.Fprintf(&buf, `<code class="%s">%s</code>`,
|
|
||||||
class, line)
|
|
||||||
}
|
|
||||||
buf.WriteString(`</pre>`)
|
|
||||||
}
|
|
||||||
text = buf.String()
|
|
||||||
} else if text != "" {
|
|
||||||
text = template.HTMLEscapeString(text)
|
|
||||||
text = fmt.Sprintf(
|
|
||||||
`<pre class="codeblock"><code>%s</code></pre>`, text)
|
|
||||||
}
|
|
||||||
viewutil.ExecutePage(meta, chainPrimitiveDiff, primitiveDiffData{
|
viewutil.ExecutePage(meta, chainPrimitiveDiff, primitiveDiffData{
|
||||||
BaseData: &viewutil.BaseData{},
|
BaseData: &viewutil.BaseData{},
|
||||||
HyphaName: h.CanonicalName(),
|
HyphaName: h.CanonicalName(),
|
||||||
Hash: hash,
|
Hash: hash,
|
||||||
Text: template.HTML(text),
|
Text: text,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
{{define "diff for at title"}}Diff of {{beautifulName .HyphaName}} at {{.Hash}}{{end}}
|
{{define "diff for at title"}}Diff of {{beautifulName .HyphaName}} at {{.Hash}}{{end}}
|
||||||
{{define "diff for at heading"}}Diff of <a href="/hypha/{{.HyphaName}}">{{beautifulName .HyphaName}}</a> at {{.Hash}}{{end}}
|
{{define "diff for at heading"}}Diff of <a href="/hypha/{{.HyphaName}}">{{beautifulName .HyphaName}}</a> at {{.Hash}}{{end}}
|
||||||
{{define "no text diff available"}}No text diff available.{{end}}
|
|
||||||
{{define "title"}}{{template "diff for at title" .}}{{end}}
|
{{define "title"}}{{template "diff for at title" .}}{{end}}
|
||||||
{{define "body"}}
|
{{define "body"}}
|
||||||
<main class="main-width">
|
<main class="main-width">
|
||||||
<article>
|
<article>
|
||||||
<h1>{{template "diff for at heading" .}}</h1>
|
<h1>{{template "diff for at heading" .}}</h1>
|
||||||
{{if .Text}}{{.Text}}{{else}}{{template "no text diff available" .}}{{end}}
|
<pre class="codeblock"><code>{{.Text}}</code></pre>
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
{{end}}
|
{{end}}
|
||||||
@ -24,9 +24,7 @@
|
|||||||
<time class="recent-changes__entry__time">
|
<time class="recent-changes__entry__time">
|
||||||
{{ $time.Format "15:04 UTC" }}
|
{{ $time.Format "15:04 UTC" }}
|
||||||
</time>
|
</time>
|
||||||
<span class="recent-changes__entry__message">
|
<span class="recent-changes__entry__message">{{$entry.Hash}}</span>
|
||||||
{{$entry.HyphaeDiffsHTML}}
|
|
||||||
</span>
|
|
||||||
{{ if $entry.Username | ne "anon" }}
|
{{ if $entry.Username | ne "anon" }}
|
||||||
<span class="recent-changes__entry__author">
|
<span class="recent-changes__entry__author">
|
||||||
— <a href="/hypha/{{$userHypha}}/{{$entry.Username}}" rel="author">{{$entry.Username}}</a>
|
— <a href="/hypha/{{$userHypha}}/{{$entry.Username}}" rel="author">{{$entry.Username}}</a>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/user"
|
"github.com/bouncepaw/mycorrhiza/user"
|
||||||
"github.com/bouncepaw/mycorrhiza/util"
|
"github.com/bouncepaw/mycorrhiza/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -118,7 +118,6 @@ func (hop *Op) Apply() *Op {
|
|||||||
"commit",
|
"commit",
|
||||||
"--author='"+hop.name+" <"+hop.email+">'",
|
"--author='"+hop.name+" <"+hop.email+">'",
|
||||||
"--message="+hop.userMsg,
|
"--message="+hop.userMsg,
|
||||||
"--no-gpg-sign",
|
|
||||||
)
|
)
|
||||||
gitMutex.Unlock()
|
gitMutex.Unlock()
|
||||||
return hop
|
return hop
|
||||||
|
|||||||
@ -2,72 +2,18 @@ package history
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"log"
|
||||||
"log/slog"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/cfg"
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/files"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// WithRevisions returns an HTML representation of `revs` that is meant to be inserted in a history page.
|
// Revision represents a revision, duh. Hash is usually short. Username is extracted from email.
|
||||||
func WithRevisions(hyphaName string, revs []Revision) string {
|
|
||||||
var buf strings.Builder
|
|
||||||
|
|
||||||
for _, grp := range groupRevisionsByMonth(revs) {
|
|
||||||
currentYear := grp[0].Time.Year()
|
|
||||||
currentMonth := grp[0].Time.Month()
|
|
||||||
sectionId := fmt.Sprintf("%04d-%02d", currentYear, currentMonth)
|
|
||||||
|
|
||||||
buf.WriteString(fmt.Sprintf(
|
|
||||||
`<section class="history__month">
|
|
||||||
<a href="#%s" class="history__month-anchor">
|
|
||||||
<h2 id="%s" class="history__month-title">%d %s</h2>
|
|
||||||
</a>
|
|
||||||
<ul class="history__entries">`,
|
|
||||||
sectionId, sectionId, currentYear, currentMonth.String(),
|
|
||||||
))
|
|
||||||
|
|
||||||
for _, rev := range grp {
|
|
||||||
buf.WriteString(fmt.Sprintf(
|
|
||||||
`<li class="history__entry">
|
|
||||||
<a class="history-entry" href="/rev/%s/%s">
|
|
||||||
<time class="history-entry__time">%s</time>
|
|
||||||
</a>
|
|
||||||
<span class="history-entry__hash"><a href="/primitive-diff/%s/%s">%s</a></span>
|
|
||||||
<span class="history-entry__msg">%s</span>`,
|
|
||||||
rev.Hash, hyphaName,
|
|
||||||
rev.timeToDisplay(),
|
|
||||||
rev.Hash, hyphaName, rev.Hash,
|
|
||||||
html.EscapeString(rev.Message),
|
|
||||||
))
|
|
||||||
|
|
||||||
if rev.Username != "anon" {
|
|
||||||
buf.WriteString(fmt.Sprintf(
|
|
||||||
`<span class="history-entry__author">by <a href="/hypha/%s/%s" rel="author">%s</a></span>`,
|
|
||||||
cfg.UserHypha, rev.Username, rev.Username,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteString("</li>\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteString(`</ul></section>`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Revision represents a revision of a hypha.
|
|
||||||
type Revision struct {
|
type Revision struct {
|
||||||
// Hash is usually short.
|
|
||||||
Hash string
|
Hash string
|
||||||
// Username is extracted from email.
|
|
||||||
Username string
|
Username string
|
||||||
Time time.Time
|
Time time.Time
|
||||||
Message string
|
Message string
|
||||||
@ -75,74 +21,13 @@ type Revision struct {
|
|||||||
hyphaeAffectedBuf []string
|
hyphaeAffectedBuf []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// HyphaeDiffsHTML returns a comma-separated list of diffs links of current revision for every affected file as HTML string.
|
|
||||||
func (rev Revision) HyphaeDiffsHTML() string {
|
|
||||||
entries := rev.hyphaeAffected()
|
|
||||||
if len(entries) == 1 {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
`<a href="/primitive-diff/%s/%s">%s</a>`,
|
|
||||||
rev.Hash, entries[0], rev.Hash,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf strings.Builder
|
|
||||||
for i, hyphaName := range entries {
|
|
||||||
if i > 0 {
|
|
||||||
buf.WriteString(`<span aria-hidden="true">, </span>`)
|
|
||||||
}
|
|
||||||
buf.WriteString(`<a href="/primitive-diff/`)
|
|
||||||
buf.WriteString(rev.Hash)
|
|
||||||
buf.WriteString(`/`)
|
|
||||||
buf.WriteString(hyphaName)
|
|
||||||
buf.WriteString(`">`)
|
|
||||||
if i == 0 {
|
|
||||||
buf.WriteString(rev.Hash)
|
|
||||||
buf.WriteString(" ")
|
|
||||||
}
|
|
||||||
buf.WriteString(hyphaName)
|
|
||||||
buf.WriteString(`</a>`)
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// descriptionForFeed generates a good enough HTML contents for a web feed.
|
|
||||||
func (rev *Revision) descriptionForFeed() string {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
`<p><b>%s</b> (by %s at %s)</p>
|
|
||||||
<p>Hyphae affected: %s</p>
|
|
||||||
<pre><code>%s</code></pre>`,
|
|
||||||
rev.Message, rev.Username, rev.TimeString(),
|
|
||||||
rev.HyphaeLinksHTML(),
|
|
||||||
rev.textDiff(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by this revision as HTML string.
|
|
||||||
func (rev Revision) HyphaeLinksHTML() string {
|
|
||||||
var buf strings.Builder
|
|
||||||
for i, hyphaName := range rev.hyphaeAffected() {
|
|
||||||
if i > 0 {
|
|
||||||
buf.WriteString(`<span aria-hidden="true">, <span>`)
|
|
||||||
}
|
|
||||||
|
|
||||||
urlSafeHyphaName := url.PathEscape(hyphaName)
|
|
||||||
buf.WriteString(fmt.Sprintf(`<a href="/hypha/%s">%s</a>`, urlSafeHyphaName, hyphaName))
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// gitLog calls `git log` and parses the results.
|
// gitLog calls `git log` and parses the results.
|
||||||
func gitLog(args ...string) ([]Revision, error) {
|
func gitLog(args ...string) ([]Revision, error) {
|
||||||
args = append([]string{
|
args = append([]string{
|
||||||
"log", "--abbrev-commit", "--no-merges",
|
"log", "--abbrev-commit", "--no-merges",
|
||||||
"--pretty=format:%h\t%ae\t%at\t%s",
|
"--pretty=format:%h\t%ae\t%at\t%s",
|
||||||
}, args...)
|
}, args...)
|
||||||
args = append(args, "--")
|
|
||||||
out, err := silentGitsh(args...)
|
out, err := silentGitsh(args...)
|
||||||
if strings.Contains(out.String(), "bad revision 'HEAD'") {
|
|
||||||
// Then we have no recent changes! It's a hack.
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -178,17 +63,11 @@ func (stream *recentChangesStream) next(n int) []Revision {
|
|||||||
// currHash is the last revision from the last call, so skip it
|
// currHash is the last revision from the last call, so skip it
|
||||||
args = append(args, "--skip=1", stream.currHash)
|
args = append(args, "--skip=1", stream.currHash)
|
||||||
}
|
}
|
||||||
|
// I don't think this can fail, so ignore the error
|
||||||
res, err := gitLog(args...)
|
res, _ := gitLog(args...)
|
||||||
if err != nil {
|
|
||||||
// TODO: return error
|
|
||||||
slog.Error("Failed to git log", "err", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if len(res) != 0 {
|
if len(res) != 0 {
|
||||||
stream.currHash = res[len(res)-1].Hash
|
stream.currHash = res[len(res)-1].Hash
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,14 +94,14 @@ func (stream recentChangesStream) iterator() func() (Revision, bool) {
|
|||||||
func RecentChanges(n int) []Revision {
|
func RecentChanges(n int) []Revision {
|
||||||
stream := newRecentChangesStream()
|
stream := newRecentChangesStream()
|
||||||
revs := stream.next(n)
|
revs := stream.next(n)
|
||||||
slog.Info("Found recent changes", "n", len(revs))
|
log.Printf("Found %d recent changes", len(revs))
|
||||||
return revs
|
return revs
|
||||||
}
|
}
|
||||||
|
|
||||||
// Revisions returns slice of revisions for the given hypha name, ordered most recent first.
|
// Revisions returns slice of revisions for the given hypha name, ordered most recent first.
|
||||||
func Revisions(hyphaName string) ([]Revision, error) {
|
func Revisions(hyphaName string) ([]Revision, error) {
|
||||||
revs, err := gitLog("--", hyphaName+".*")
|
revs, err := gitLog("--", hyphaName+".*")
|
||||||
slog.Info("Found revisions", "hyphaName", hyphaName, "n", len(revs), "err", err)
|
log.Printf("Found %d revisions for ‘%s’\n", len(revs), hyphaName)
|
||||||
return revs, err
|
return revs, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -373,21 +252,3 @@ func PrimitiveDiffAtRevision(filepath, hash string) (string, error) {
|
|||||||
}
|
}
|
||||||
return out.String(), err
|
return out.String(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// SplitPrimitiveDiff splits a primitive diff of a single file into hunks.
|
|
||||||
func SplitPrimitiveDiff(text string) (result []string) {
|
|
||||||
idx := strings.Index(text, "@@ -")
|
|
||||||
if idx < 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
text = text[idx:]
|
|
||||||
for {
|
|
||||||
idx = strings.Index(text, "\n@@ -")
|
|
||||||
if idx < 0 {
|
|
||||||
result = append(result, text)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result = append(result, text[:idx+1])
|
|
||||||
text = text[idx+1:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
55
history/view.qtpl
Normal file
55
history/view.qtpl
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{% import "fmt" %}
|
||||||
|
{% import "github.com/bouncepaw/mycorrhiza/cfg" %}
|
||||||
|
|
||||||
|
HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by this revision as HTML string.
|
||||||
|
{% func (rev Revision) HyphaeLinksHTML() %}
|
||||||
|
{% stripspace %}
|
||||||
|
{% for i, hyphaName := range rev.hyphaeAffected() %}
|
||||||
|
{% if i > 0 %}
|
||||||
|
<span aria-hidden="true">, </span>
|
||||||
|
{% endif %}
|
||||||
|
<a href="/hypha/{%s hyphaName %}">{%s hyphaName %}</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% endstripspace %}
|
||||||
|
{% endfunc %}
|
||||||
|
|
||||||
|
descriptionForFeed generates a good enough HTML contents for a web feed.
|
||||||
|
{% func (rev *Revision) descriptionForFeed() %}
|
||||||
|
<p><b>{%s rev.Message %}</b> (by {%s rev.Username %} at {%s rev.TimeString() %})</p>
|
||||||
|
<p>Hyphae affected: {%= rev.HyphaeLinksHTML() %}</p>
|
||||||
|
<pre><code>{%s rev.textDiff() %}</code></pre>
|
||||||
|
{% endfunc %}
|
||||||
|
|
||||||
|
WithRevisions returns an html representation of `revs` that is meant to be inserted in a history page.
|
||||||
|
{% func WithRevisions(hyphaName string, revs []Revision) %}
|
||||||
|
{% for _, grp := range groupRevisionsByMonth(revs) %}
|
||||||
|
{% code
|
||||||
|
currentYear := grp[0].Time.Year()
|
||||||
|
currentMonth := grp[0].Time.Month()
|
||||||
|
sectionId := fmt.Sprintf("%04d-%02d", currentYear, currentMonth)
|
||||||
|
%}
|
||||||
|
<section class="history__month">
|
||||||
|
<a href="#{%s sectionId %}" class="history__month-anchor">
|
||||||
|
<h2 id="{%s sectionId %}" class="history__month-title">{%d currentYear %} {%s currentMonth.String() %}</h2>
|
||||||
|
</a>
|
||||||
|
<ul class="history__entries">
|
||||||
|
{% for _, rev := range grp %}
|
||||||
|
{%= rev.asHistoryEntry(hyphaName) %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfunc %}
|
||||||
|
|
||||||
|
{% func (rev *Revision) asHistoryEntry(hyphaName string) %}
|
||||||
|
<li class="history__entry">
|
||||||
|
<a class="history-entry" href="/rev/{%s rev.Hash %}/{%s hyphaName %}">
|
||||||
|
<time class="history-entry__time">{%s rev.timeToDisplay() %}</time>
|
||||||
|
</a>
|
||||||
|
<span class="history-entry__hash"><a href="/primitive-diff/{%s rev.Hash %}/{%s hyphaName %}">{%s rev.Hash %}</a></span>
|
||||||
|
<span class="history-entry__msg">{%s rev.Message %}</span>
|
||||||
|
{% if rev.Username != "anon" %}
|
||||||
|
<span class="history-entry__author">by <a href="/hypha/{%s cfg.UserHypha %}/{%s rev.Username %}" rel="author">{%s rev.Username %}</a></span>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfunc %}
|
||||||
326
history/view.qtpl.go
Normal file
326
history/view.qtpl.go
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
// Code generated by qtc from "view.qtpl". DO NOT EDIT.
|
||||||
|
// See https://github.com/valyala/quicktemplate for details.
|
||||||
|
|
||||||
|
//line history/view.qtpl:1
|
||||||
|
package history
|
||||||
|
|
||||||
|
//line history/view.qtpl:1
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
//line history/view.qtpl:2
|
||||||
|
import "github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
|
|
||||||
|
// HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by this revision as HTML string.
|
||||||
|
|
||||||
|
//line history/view.qtpl:5
|
||||||
|
import (
|
||||||
|
qtio422016 "io"
|
||||||
|
|
||||||
|
qt422016 "github.com/valyala/quicktemplate"
|
||||||
|
)
|
||||||
|
|
||||||
|
//line history/view.qtpl:5
|
||||||
|
var (
|
||||||
|
_ = qtio422016.Copy
|
||||||
|
_ = qt422016.AcquireByteBuffer
|
||||||
|
)
|
||||||
|
|
||||||
|
//line history/view.qtpl:5
|
||||||
|
func (rev Revision) StreamHyphaeLinksHTML(qw422016 *qt422016.Writer) {
|
||||||
|
//line history/view.qtpl:5
|
||||||
|
qw422016.N().S(`
|
||||||
|
`)
|
||||||
|
//line history/view.qtpl:7
|
||||||
|
for i, hyphaName := range rev.hyphaeAffected() {
|
||||||
|
//line history/view.qtpl:8
|
||||||
|
if i > 0 {
|
||||||
|
//line history/view.qtpl:8
|
||||||
|
qw422016.N().S(`<span aria-hidden="true">, </span>`)
|
||||||
|
//line history/view.qtpl:10
|
||||||
|
}
|
||||||
|
//line history/view.qtpl:10
|
||||||
|
qw422016.N().S(`<a href="/hypha/`)
|
||||||
|
//line history/view.qtpl:11
|
||||||
|
qw422016.E().S(hyphaName)
|
||||||
|
//line history/view.qtpl:11
|
||||||
|
qw422016.N().S(`">`)
|
||||||
|
//line history/view.qtpl:11
|
||||||
|
qw422016.E().S(hyphaName)
|
||||||
|
//line history/view.qtpl:11
|
||||||
|
qw422016.N().S(`</a>`)
|
||||||
|
//line history/view.qtpl:12
|
||||||
|
}
|
||||||
|
//line history/view.qtpl:13
|
||||||
|
qw422016.N().S(`
|
||||||
|
`)
|
||||||
|
//line history/view.qtpl:14
|
||||||
|
}
|
||||||
|
|
||||||
|
//line history/view.qtpl:14
|
||||||
|
func (rev Revision) WriteHyphaeLinksHTML(qq422016 qtio422016.Writer) {
|
||||||
|
//line history/view.qtpl:14
|
||||||
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
|
//line history/view.qtpl:14
|
||||||
|
rev.StreamHyphaeLinksHTML(qw422016)
|
||||||
|
//line history/view.qtpl:14
|
||||||
|
qt422016.ReleaseWriter(qw422016)
|
||||||
|
//line history/view.qtpl:14
|
||||||
|
}
|
||||||
|
|
||||||
|
//line history/view.qtpl:14
|
||||||
|
func (rev Revision) HyphaeLinksHTML() string {
|
||||||
|
//line history/view.qtpl:14
|
||||||
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
|
//line history/view.qtpl:14
|
||||||
|
rev.WriteHyphaeLinksHTML(qb422016)
|
||||||
|
//line history/view.qtpl:14
|
||||||
|
qs422016 := string(qb422016.B)
|
||||||
|
//line history/view.qtpl:14
|
||||||
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
|
//line history/view.qtpl:14
|
||||||
|
return qs422016
|
||||||
|
//line history/view.qtpl:14
|
||||||
|
}
|
||||||
|
|
||||||
|
// descriptionForFeed generates a good enough HTML contents for a web feed.
|
||||||
|
|
||||||
|
//line history/view.qtpl:17
|
||||||
|
func (rev *Revision) streamdescriptionForFeed(qw422016 *qt422016.Writer) {
|
||||||
|
//line history/view.qtpl:17
|
||||||
|
qw422016.N().S(`
|
||||||
|
<p><b>`)
|
||||||
|
//line history/view.qtpl:18
|
||||||
|
qw422016.E().S(rev.Message)
|
||||||
|
//line history/view.qtpl:18
|
||||||
|
qw422016.N().S(`</b> (by `)
|
||||||
|
//line history/view.qtpl:18
|
||||||
|
qw422016.E().S(rev.Username)
|
||||||
|
//line history/view.qtpl:18
|
||||||
|
qw422016.N().S(` at `)
|
||||||
|
//line history/view.qtpl:18
|
||||||
|
qw422016.E().S(rev.TimeString())
|
||||||
|
//line history/view.qtpl:18
|
||||||
|
qw422016.N().S(`)</p>
|
||||||
|
<p>Hyphae affected: `)
|
||||||
|
//line history/view.qtpl:19
|
||||||
|
rev.StreamHyphaeLinksHTML(qw422016)
|
||||||
|
//line history/view.qtpl:19
|
||||||
|
qw422016.N().S(`</p>
|
||||||
|
<pre><code>`)
|
||||||
|
//line history/view.qtpl:20
|
||||||
|
qw422016.E().S(rev.textDiff())
|
||||||
|
//line history/view.qtpl:20
|
||||||
|
qw422016.N().S(`</code></pre>
|
||||||
|
`)
|
||||||
|
//line history/view.qtpl:21
|
||||||
|
}
|
||||||
|
|
||||||
|
//line history/view.qtpl:21
|
||||||
|
func (rev *Revision) writedescriptionForFeed(qq422016 qtio422016.Writer) {
|
||||||
|
//line history/view.qtpl:21
|
||||||
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
|
//line history/view.qtpl:21
|
||||||
|
rev.streamdescriptionForFeed(qw422016)
|
||||||
|
//line history/view.qtpl:21
|
||||||
|
qt422016.ReleaseWriter(qw422016)
|
||||||
|
//line history/view.qtpl:21
|
||||||
|
}
|
||||||
|
|
||||||
|
//line history/view.qtpl:21
|
||||||
|
func (rev *Revision) descriptionForFeed() string {
|
||||||
|
//line history/view.qtpl:21
|
||||||
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
|
//line history/view.qtpl:21
|
||||||
|
rev.writedescriptionForFeed(qb422016)
|
||||||
|
//line history/view.qtpl:21
|
||||||
|
qs422016 := string(qb422016.B)
|
||||||
|
//line history/view.qtpl:21
|
||||||
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
|
//line history/view.qtpl:21
|
||||||
|
return qs422016
|
||||||
|
//line history/view.qtpl:21
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRevisions returns an html representation of `revs` that is meant to be inserted in a history page.
|
||||||
|
|
||||||
|
//line history/view.qtpl:24
|
||||||
|
func StreamWithRevisions(qw422016 *qt422016.Writer, hyphaName string, revs []Revision) {
|
||||||
|
//line history/view.qtpl:24
|
||||||
|
qw422016.N().S(`
|
||||||
|
`)
|
||||||
|
//line history/view.qtpl:25
|
||||||
|
for _, grp := range groupRevisionsByMonth(revs) {
|
||||||
|
//line history/view.qtpl:25
|
||||||
|
qw422016.N().S(`
|
||||||
|
`)
|
||||||
|
//line history/view.qtpl:27
|
||||||
|
currentYear := grp[0].Time.Year()
|
||||||
|
currentMonth := grp[0].Time.Month()
|
||||||
|
sectionId := fmt.Sprintf("%04d-%02d", currentYear, currentMonth)
|
||||||
|
|
||||||
|
//line history/view.qtpl:30
|
||||||
|
qw422016.N().S(`
|
||||||
|
<section class="history__month">
|
||||||
|
<a href="#`)
|
||||||
|
//line history/view.qtpl:32
|
||||||
|
qw422016.E().S(sectionId)
|
||||||
|
//line history/view.qtpl:32
|
||||||
|
qw422016.N().S(`" class="history__month-anchor">
|
||||||
|
<h2 id="`)
|
||||||
|
//line history/view.qtpl:33
|
||||||
|
qw422016.E().S(sectionId)
|
||||||
|
//line history/view.qtpl:33
|
||||||
|
qw422016.N().S(`" class="history__month-title">`)
|
||||||
|
//line history/view.qtpl:33
|
||||||
|
qw422016.N().D(currentYear)
|
||||||
|
//line history/view.qtpl:33
|
||||||
|
qw422016.N().S(` `)
|
||||||
|
//line history/view.qtpl:33
|
||||||
|
qw422016.E().S(currentMonth.String())
|
||||||
|
//line history/view.qtpl:33
|
||||||
|
qw422016.N().S(`</h2>
|
||||||
|
</a>
|
||||||
|
<ul class="history__entries">
|
||||||
|
`)
|
||||||
|
//line history/view.qtpl:36
|
||||||
|
for _, rev := range grp {
|
||||||
|
//line history/view.qtpl:36
|
||||||
|
qw422016.N().S(`
|
||||||
|
`)
|
||||||
|
//line history/view.qtpl:37
|
||||||
|
rev.streamasHistoryEntry(qw422016, hyphaName)
|
||||||
|
//line history/view.qtpl:37
|
||||||
|
qw422016.N().S(`
|
||||||
|
`)
|
||||||
|
//line history/view.qtpl:38
|
||||||
|
}
|
||||||
|
//line history/view.qtpl:38
|
||||||
|
qw422016.N().S(`
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
`)
|
||||||
|
//line history/view.qtpl:41
|
||||||
|
}
|
||||||
|
//line history/view.qtpl:41
|
||||||
|
qw422016.N().S(`
|
||||||
|
`)
|
||||||
|
//line history/view.qtpl:42
|
||||||
|
}
|
||||||
|
|
||||||
|
//line history/view.qtpl:42
|
||||||
|
func WriteWithRevisions(qq422016 qtio422016.Writer, hyphaName string, revs []Revision) {
|
||||||
|
//line history/view.qtpl:42
|
||||||
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
|
//line history/view.qtpl:42
|
||||||
|
StreamWithRevisions(qw422016, hyphaName, revs)
|
||||||
|
//line history/view.qtpl:42
|
||||||
|
qt422016.ReleaseWriter(qw422016)
|
||||||
|
//line history/view.qtpl:42
|
||||||
|
}
|
||||||
|
|
||||||
|
//line history/view.qtpl:42
|
||||||
|
func WithRevisions(hyphaName string, revs []Revision) string {
|
||||||
|
//line history/view.qtpl:42
|
||||||
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
|
//line history/view.qtpl:42
|
||||||
|
WriteWithRevisions(qb422016, hyphaName, revs)
|
||||||
|
//line history/view.qtpl:42
|
||||||
|
qs422016 := string(qb422016.B)
|
||||||
|
//line history/view.qtpl:42
|
||||||
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
|
//line history/view.qtpl:42
|
||||||
|
return qs422016
|
||||||
|
//line history/view.qtpl:42
|
||||||
|
}
|
||||||
|
|
||||||
|
//line history/view.qtpl:44
|
||||||
|
func (rev *Revision) streamasHistoryEntry(qw422016 *qt422016.Writer, hyphaName string) {
|
||||||
|
//line history/view.qtpl:44
|
||||||
|
qw422016.N().S(`
|
||||||
|
<li class="history__entry">
|
||||||
|
<a class="history-entry" href="/rev/`)
|
||||||
|
//line history/view.qtpl:46
|
||||||
|
qw422016.E().S(rev.Hash)
|
||||||
|
//line history/view.qtpl:46
|
||||||
|
qw422016.N().S(`/`)
|
||||||
|
//line history/view.qtpl:46
|
||||||
|
qw422016.E().S(hyphaName)
|
||||||
|
//line history/view.qtpl:46
|
||||||
|
qw422016.N().S(`">
|
||||||
|
<time class="history-entry__time">`)
|
||||||
|
//line history/view.qtpl:47
|
||||||
|
qw422016.E().S(rev.timeToDisplay())
|
||||||
|
//line history/view.qtpl:47
|
||||||
|
qw422016.N().S(`</time>
|
||||||
|
</a>
|
||||||
|
<span class="history-entry__hash"><a href="/primitive-diff/`)
|
||||||
|
//line history/view.qtpl:49
|
||||||
|
qw422016.E().S(rev.Hash)
|
||||||
|
//line history/view.qtpl:49
|
||||||
|
qw422016.N().S(`/`)
|
||||||
|
//line history/view.qtpl:49
|
||||||
|
qw422016.E().S(hyphaName)
|
||||||
|
//line history/view.qtpl:49
|
||||||
|
qw422016.N().S(`">`)
|
||||||
|
//line history/view.qtpl:49
|
||||||
|
qw422016.E().S(rev.Hash)
|
||||||
|
//line history/view.qtpl:49
|
||||||
|
qw422016.N().S(`</a></span>
|
||||||
|
<span class="history-entry__msg">`)
|
||||||
|
//line history/view.qtpl:50
|
||||||
|
qw422016.E().S(rev.Message)
|
||||||
|
//line history/view.qtpl:50
|
||||||
|
qw422016.N().S(`</span>
|
||||||
|
`)
|
||||||
|
//line history/view.qtpl:51
|
||||||
|
if rev.Username != "anon" {
|
||||||
|
//line history/view.qtpl:51
|
||||||
|
qw422016.N().S(`
|
||||||
|
<span class="history-entry__author">by <a href="/hypha/`)
|
||||||
|
//line history/view.qtpl:52
|
||||||
|
qw422016.E().S(cfg.UserHypha)
|
||||||
|
//line history/view.qtpl:52
|
||||||
|
qw422016.N().S(`/`)
|
||||||
|
//line history/view.qtpl:52
|
||||||
|
qw422016.E().S(rev.Username)
|
||||||
|
//line history/view.qtpl:52
|
||||||
|
qw422016.N().S(`" rel="author">`)
|
||||||
|
//line history/view.qtpl:52
|
||||||
|
qw422016.E().S(rev.Username)
|
||||||
|
//line history/view.qtpl:52
|
||||||
|
qw422016.N().S(`</a></span>
|
||||||
|
`)
|
||||||
|
//line history/view.qtpl:53
|
||||||
|
}
|
||||||
|
//line history/view.qtpl:53
|
||||||
|
qw422016.N().S(`
|
||||||
|
</li>
|
||||||
|
`)
|
||||||
|
//line history/view.qtpl:55
|
||||||
|
}
|
||||||
|
|
||||||
|
//line history/view.qtpl:55
|
||||||
|
func (rev *Revision) writeasHistoryEntry(qq422016 qtio422016.Writer, hyphaName string) {
|
||||||
|
//line history/view.qtpl:55
|
||||||
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
|
//line history/view.qtpl:55
|
||||||
|
rev.streamasHistoryEntry(qw422016, hyphaName)
|
||||||
|
//line history/view.qtpl:55
|
||||||
|
qt422016.ReleaseWriter(qw422016)
|
||||||
|
//line history/view.qtpl:55
|
||||||
|
}
|
||||||
|
|
||||||
|
//line history/view.qtpl:55
|
||||||
|
func (rev *Revision) asHistoryEntry(hyphaName string) string {
|
||||||
|
//line history/view.qtpl:55
|
||||||
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
|
//line history/view.qtpl:55
|
||||||
|
rev.writeasHistoryEntry(qb422016, hyphaName)
|
||||||
|
//line history/view.qtpl:55
|
||||||
|
qs422016 := string(qb422016.B)
|
||||||
|
//line history/view.qtpl:55
|
||||||
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
|
//line history/view.qtpl:55
|
||||||
|
return qs422016
|
||||||
|
//line history/view.qtpl:55
|
||||||
|
}
|
||||||
61
httpd.go
61
httpd.go
@ -1,18 +1,17 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"log"
|
||||||
"log/slog"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/cfg"
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func serveHTTP(handler http.Handler) (err error) {
|
func serveHTTP(handler http.Handler) {
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
ReadTimeout: 300 * time.Second,
|
ReadTimeout: 300 * time.Second,
|
||||||
WriteTimeout: 300 * time.Second,
|
WriteTimeout: 300 * time.Second,
|
||||||
@ -21,51 +20,35 @@ func serveHTTP(handler http.Handler) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(cfg.ListenAddr, "/") {
|
if strings.HasPrefix(cfg.ListenAddr, "/") {
|
||||||
err = startUnixSocketServer(server, cfg.ListenAddr)
|
startUnixSocketServer(server, cfg.ListenAddr)
|
||||||
} else {
|
} else {
|
||||||
server.Addr = cfg.ListenAddr
|
server.Addr = cfg.ListenAddr
|
||||||
err = startHTTPServer(server)
|
startHTTPServer(server)
|
||||||
}
|
}
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func startUnixSocketServer(server *http.Server, socketPath string) error {
|
func startUnixSocketServer(server *http.Server, socketFile string) {
|
||||||
err := os.Remove(socketPath)
|
os.Remove(socketFile)
|
||||||
|
|
||||||
|
listener, err := net.Listen("unix", socketFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("Failed to clean up old socket", "err", err)
|
log.Fatalf("Failed to start a server: %v", err)
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
if err := os.Chmod(socketFile, 0666); err != nil {
|
||||||
|
log.Fatalf("Failed to set socket permissions: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
listener, err := net.Listen("unix", socketPath)
|
log.Printf("Listening on Unix socket %s", cfg.ListenAddr)
|
||||||
if err != nil {
|
if err := server.Serve(listener); err != http.ErrServerClosed {
|
||||||
slog.Error("Failed to start the server", "err", err)
|
log.Fatalf("Failed to start a server: %v", err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
defer func(listener net.Listener) {
|
|
||||||
_ = listener.Close()
|
|
||||||
}(listener)
|
|
||||||
|
|
||||||
if err := os.Chmod(socketPath, 0666); err != nil {
|
|
||||||
slog.Error("Failed to set socket permissions", "err", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Info("Listening Unix socket", "addr", socketPath)
|
|
||||||
|
|
||||||
if err := server.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
|
|
||||||
slog.Error("Failed to start the server", "err", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func startHTTPServer(server *http.Server) error {
|
func startHTTPServer(server *http.Server) {
|
||||||
slog.Info("Listening over HTTP", "addr", server.Addr)
|
log.Printf("Listening on %s", server.Addr)
|
||||||
|
if err := server.ListenAndServe(); err != http.ErrServerClosed {
|
||||||
if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
log.Fatalf("Failed to start a server: %v", err)
|
||||||
slog.Error("Failed to start the server", "err", err)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
package hyphae
|
package hyphae
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bouncepaw/mycorrhiza/util"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExistingHypha is not EmptyHypha. *MediaHypha and *TextualHypha implement this interface.
|
// ExistingHypha is not EmptyHypha. *MediaHypha and *TextualHypha implement this interface.
|
||||||
@ -1,11 +1,11 @@
|
|||||||
package hyphae
|
package hyphae
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/mimetype"
|
"github.com/bouncepaw/mycorrhiza/mimetype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Index finds all hypha files in the full `path` and saves them to the hypha storage.
|
// Index finds all hypha files in the full `path` and saves them to the hypha storage.
|
||||||
@ -27,10 +27,11 @@ func Index(path string) {
|
|||||||
switch foundHypha := foundHypha.(type) {
|
switch foundHypha := foundHypha.(type) {
|
||||||
case *TextualHypha: // conflict! overwrite
|
case *TextualHypha: // conflict! overwrite
|
||||||
storedHypha.mycoFilePath = foundHypha.mycoFilePath
|
storedHypha.mycoFilePath = foundHypha.mycoFilePath
|
||||||
slog.Info("File collision",
|
log.Printf(
|
||||||
"hypha", foundHypha.CanonicalName(),
|
"File collision for hypha ‘%s’, using ‘%s’ rather than ‘%s’\n",
|
||||||
"usingFile", foundHypha.TextFilePath(),
|
foundHypha.CanonicalName(),
|
||||||
"insteadOf", storedHypha.TextFilePath(),
|
foundHypha.TextFilePath(),
|
||||||
|
storedHypha.TextFilePath(),
|
||||||
)
|
)
|
||||||
case *MediaHypha: // no conflict
|
case *MediaHypha: // no conflict
|
||||||
Insert(ExtendTextualToMedia(storedHypha, foundHypha.mediaFilePath))
|
Insert(ExtendTextualToMedia(storedHypha, foundHypha.mediaFilePath))
|
||||||
@ -42,16 +43,16 @@ func Index(path string) {
|
|||||||
storedHypha.mycoFilePath = foundHypha.mycoFilePath
|
storedHypha.mycoFilePath = foundHypha.mycoFilePath
|
||||||
case *MediaHypha: // conflict! overwrite
|
case *MediaHypha: // conflict! overwrite
|
||||||
storedHypha.mediaFilePath = foundHypha.mediaFilePath
|
storedHypha.mediaFilePath = foundHypha.mediaFilePath
|
||||||
|
log.Printf(
|
||||||
slog.Info("File collision",
|
"File collision for hypha ‘%s’, using ‘%s’ rather than ‘%s’\n",
|
||||||
"hypha", foundHypha.CanonicalName(),
|
foundHypha.CanonicalName(),
|
||||||
"usingFile", foundHypha.MediaFilePath(),
|
foundHypha.MediaFilePath(),
|
||||||
"insteadOf", storedHypha.MediaFilePath(),
|
storedHypha.MediaFilePath(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
slog.Info("Indexed hyphae", "n", Count())
|
log.Println("Indexed", Count(), "hyphae")
|
||||||
}
|
}
|
||||||
|
|
||||||
// indexHelper finds all hypha files in the full `path` and sends them to the
|
// indexHelper finds all hypha files in the full `path` and sends them to the
|
||||||
@ -60,8 +61,7 @@ func Index(path string) {
|
|||||||
func indexHelper(path string, nestLevel uint, ch chan ExistingHypha) {
|
func indexHelper(path string, nestLevel uint, ch chan ExistingHypha) {
|
||||||
nodes, err := os.ReadDir(path)
|
nodes, err := os.ReadDir(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to read directory", "path", path, "err", err)
|
log.Fatal(err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
@ -73,7 +73,7 @@ func indexHelper(path string, nestLevel uint, ch chan ExistingHypha) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
hyphaPartPath = filepath.ToSlash(filepath.Join(path, node.Name()))
|
hyphaPartPath = filepath.Join(path, node.Name())
|
||||||
hyphaName, isText, skip = mimetype.DataFromFilename(hyphaPartPath)
|
hyphaName, isText, skip = mimetype.DataFromFilename(hyphaPartPath)
|
||||||
)
|
)
|
||||||
if !skip {
|
if !skip {
|
||||||
@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// hyphaNamePattern is a pattern which all hyphae names must match.
|
// hyphaNamePattern is a pattern which all hyphae names must match.
|
||||||
var hyphaNamePattern = regexp.MustCompile(`^[^?!:#@><*|"'&%{}]+$`)
|
var hyphaNamePattern = regexp.MustCompile(`[^?!:#@><*|"'&%{}]+`)
|
||||||
|
|
||||||
// IsValidName checks for invalid characters and path traversals.
|
// IsValidName checks for invalid characters and path traversals.
|
||||||
func IsValidName(hyphaName string) bool {
|
func IsValidName(hyphaName string) bool {
|
||||||
55
hyphae/iteration/iteration.go
Normal file
55
hyphae/iteration/iteration.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// Package iteration provides a handy API for making multiple checks on all hyphae in one go.
|
||||||
|
package iteration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Iteration represents an iteration over all existing hyphae in the storage. Iteration is done on all existing hyphae. The order of hyphae is not specified. For all hyphae, checks are made.
|
||||||
|
type Iteration struct {
|
||||||
|
sync.Mutex
|
||||||
|
checks []func(h hyphae.Hypha) CheckResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIteration constructs an iteration without checks.
|
||||||
|
func NewIteration() *Iteration {
|
||||||
|
return &Iteration{
|
||||||
|
checks: make([]func(h hyphae.Hypha) CheckResult, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCheck adds the check to the iteration. It is concurrent-safe. Checks are meant to have side-effects.
|
||||||
|
func (i7n *Iteration) AddCheck(check func(h hyphae.Hypha) CheckResult) {
|
||||||
|
i7n.Lock()
|
||||||
|
i7n.checks = append(i7n.checks, check)
|
||||||
|
i7n.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i7n *Iteration) removeCheck(i int) {
|
||||||
|
i7n.checks[i] = i7n.checks[len(i7n.checks)-1]
|
||||||
|
i7n.checks = i7n.checks[:len(i7n.checks)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignite does the iteration by walking over all hyphae yielded by the iterator used and calling all checks on the hypha. Ignited iterations are not concurrent-safe.
|
||||||
|
//
|
||||||
|
// After ignition, you should not use the same Iteration again.
|
||||||
|
func (i7n *Iteration) Ignite() {
|
||||||
|
for h := range hyphae.YieldExistingHyphae() {
|
||||||
|
for i, check := range i7n.checks {
|
||||||
|
if res := check(h); res == CheckForgetMe {
|
||||||
|
i7n.removeCheck(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckResult is a result of an iteration check.
|
||||||
|
type CheckResult int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// CheckContinue is returned when the check wants to be used next time too.
|
||||||
|
CheckContinue CheckResult = iota
|
||||||
|
// CheckForgetMe is returned when the check wants to be forgotten and not used anymore.
|
||||||
|
CheckForgetMe
|
||||||
|
)
|
||||||
@ -1,10 +1,9 @@
|
|||||||
package hyphae
|
package hyphae
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/files"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type MediaHypha struct {
|
type MediaHypha struct {
|
||||||
@ -2,53 +2,96 @@ package hypview
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"html/template"
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
"log/slog"
|
"github.com/bouncepaw/mycorrhiza/viewutil"
|
||||||
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/backlinks"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/cfg"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/web/viewutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
//go:embed *.html
|
//go:embed *.html
|
||||||
fs embed.FS
|
fs embed.FS
|
||||||
ruTranslation = `
|
ruTranslation = `
|
||||||
|
{{define "empty heading"}}Эта гифа не существует{{end}}
|
||||||
|
{{define "empty no rights"}}У вас нет прав для создания новых гиф. Вы можете:{{end}}
|
||||||
|
{{define "empty log in"}}Войти в свою учётную запись, если она у вас есть{{end}}
|
||||||
|
{{define "empty register"}}Создать новую учётную запись{{end}}
|
||||||
|
{{define "write a text"}}Написать текст{{end}}
|
||||||
|
{{define "write a text tip"}}Напишите заметку, дневник, статью, рассказ или иной текст с помощью <a href="/help/en/mycomarkup" class="shy-link">микоразметки</a>. Сохраняется полная история правок документа.{{end}}
|
||||||
|
{{define "write a text writing conventions"}}Не забывайте следовать правилам оформления этой вики, если они имеются.{{end}}
|
||||||
|
{{define "write a text btn"}}Создать{{end}}
|
||||||
|
{{define "upload a media"}}Загрузить медиа{{end}}
|
||||||
|
{{define "upload a media tip"}}Загрузите изображение, видео или аудио. Распространённые форматы можно просматривать из браузера, остальные – просто скачать. Позже вы можете дописать пояснение к этому медиа.{{end}}
|
||||||
|
{{define "upload a media btn"}}Загрузить{{end}}
|
||||||
|
|
||||||
|
{{define "delete hypha?"}}Удалить {{beautifulName .}}?{{end}}
|
||||||
|
{{define "delete [[hypha]]?"}}Удалить <a href="/hypha/{{.}}">{{beautifulName .}}</a>?{{end}}
|
||||||
|
{{define "want to delete?"}}Вы действительно хотите удалить эту гифу?{{end}}
|
||||||
|
{{define "delete tip"}}Нельзя отменить удаление гифы, но её история останется доступной.{{end}}
|
||||||
|
|
||||||
{{define "rename hypha?"}}Переименовать {{beautifulName .}}?{{end}}
|
{{define "rename hypha?"}}Переименовать {{beautifulName .}}?{{end}}
|
||||||
{{define "rename [[hypha]]?"}}Переименовать <a href="/hypha/{{.}}">{{beautifulName .}}</a>?{{end}}
|
{{define "rename [[hypha]]?"}}Переименовать <a href="/hypha/{{.}}">{{beautifulName .}}</a>?{{end}}
|
||||||
{{define "new name"}}Новое название:{{end}}
|
{{define "new name"}}Новое название:{{end}}
|
||||||
{{define "rename recursively"}}Также переименовать подгифы{{end}}
|
{{define "rename recursively"}}Также переименовать подгифы{{end}}
|
||||||
{{define "rename tip"}}Переименовывайте аккуратно. <a href="/help/en/rename">Документация на английском.</a>{{end}}
|
{{define "rename tip"}}Переименовывайте аккуратно. <a href="/help/en/rename">Документация на английском.</a>{{end}}
|
||||||
{{define "leave redirection"}}Оставить перенаправление{{end}}
|
{{define "leave redirections"}}Оставить перенаправления{{end}}
|
||||||
|
|
||||||
|
|
||||||
`
|
`
|
||||||
chainNaviTitle viewutil.Chain
|
chainNaviTitle viewutil.Chain
|
||||||
|
chainEmptyHypha viewutil.Chain
|
||||||
|
chainDeleteHypha viewutil.Chain
|
||||||
chainRenameHypha viewutil.Chain
|
chainRenameHypha viewutil.Chain
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
chainNaviTitle = viewutil.CopyEnRuWith(fs, "view_navititle.html", "")
|
chainNaviTitle = viewutil.CopyEnRuWith(fs, "view_navititle.html", "")
|
||||||
|
chainEmptyHypha = viewutil.CopyEnRuWith(fs, "view_empty_hypha.html", ruTranslation)
|
||||||
|
chainDeleteHypha = viewutil.CopyEnRuWith(fs, "view_delete.html", ruTranslation)
|
||||||
chainRenameHypha = viewutil.CopyEnRuWith(fs, "view_rename.html", ruTranslation)
|
chainRenameHypha = viewutil.CopyEnRuWith(fs, "view_rename.html", ruTranslation)
|
||||||
}
|
}
|
||||||
|
|
||||||
type renameData struct {
|
type deleteRenameData struct {
|
||||||
*viewutil.BaseData
|
*viewutil.BaseData
|
||||||
HyphaName string
|
HyphaName string
|
||||||
LeaveRedirectionDefault bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RenameHypha(meta viewutil.Meta, hyphaName string) {
|
func RenameHypha(meta viewutil.Meta, hyphaName string) {
|
||||||
viewutil.ExecutePage(meta, chainRenameHypha, renameData{
|
viewutil.ExecutePage(meta, chainRenameHypha, deleteRenameData{
|
||||||
BaseData: &viewutil.BaseData{
|
BaseData: &viewutil.BaseData{
|
||||||
Addr: "/rename/" + hyphaName,
|
Addr: "/rename/" + hyphaName,
|
||||||
},
|
},
|
||||||
HyphaName: hyphaName,
|
HyphaName: hyphaName,
|
||||||
LeaveRedirectionDefault: backlinks.BacklinksCount(hyphaName) != 0,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeleteHypha(meta viewutil.Meta, hyphaName string) {
|
||||||
|
viewutil.ExecutePage(meta, chainDeleteHypha, deleteRenameData{
|
||||||
|
BaseData: &viewutil.BaseData{
|
||||||
|
Addr: "/delete/" + hyphaName,
|
||||||
|
},
|
||||||
|
HyphaName: hyphaName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type emptyHyphaData struct {
|
||||||
|
Meta viewutil.Meta
|
||||||
|
HyphaName string
|
||||||
|
AllowRegistration bool
|
||||||
|
UseAuth bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func EmptyHypha(meta viewutil.Meta, hyphaName string) string {
|
||||||
|
var buf strings.Builder
|
||||||
|
if err := chainEmptyHypha.Get(meta).ExecuteTemplate(&buf, "empty hypha card", emptyHyphaData{
|
||||||
|
Meta: meta,
|
||||||
|
HyphaName: hyphaName,
|
||||||
|
AllowRegistration: cfg.AllowRegistration,
|
||||||
|
UseAuth: cfg.UseAuth,
|
||||||
|
}); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
type naviTitleData struct {
|
type naviTitleData struct {
|
||||||
HyphaNameParts []string
|
HyphaNameParts []string
|
||||||
HyphaNamePartsWithParents []string
|
HyphaNamePartsWithParents []string
|
||||||
@ -56,7 +99,7 @@ type naviTitleData struct {
|
|||||||
HomeHypha string
|
HomeHypha string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NaviTitle(meta viewutil.Meta, hyphaName string) template.HTML {
|
func NaviTitle(meta viewutil.Meta, hyphaName string) string {
|
||||||
parts, partsWithParents := naviTitleify(hyphaName)
|
parts, partsWithParents := naviTitleify(hyphaName)
|
||||||
var buf strings.Builder
|
var buf strings.Builder
|
||||||
err := chainNaviTitle.Get(meta).ExecuteTemplate(&buf, "navititle", naviTitleData{
|
err := chainNaviTitle.Get(meta).ExecuteTemplate(&buf, "navititle", naviTitleData{
|
||||||
@ -66,9 +109,9 @@ func NaviTitle(meta viewutil.Meta, hyphaName string) template.HTML {
|
|||||||
HomeHypha: cfg.HomeHypha,
|
HomeHypha: cfg.HomeHypha,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to render NaviTitle properly; using nevertheless", "err", err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
return template.HTML(buf.String())
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func naviTitleify(hyphaName string) ([]string, []string) {
|
func naviTitleify(hyphaName string) ([]string, []string) {
|
||||||
|
|||||||
32
hypview/view_empty_hypha.html
Normal file
32
hypview/view_empty_hypha.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{{define "empty hypha card"}}
|
||||||
|
<section class="non-existent-hypha">
|
||||||
|
<h2 class="non-existent-hypha__title">{{block "empty heading" .}}This hypha does not exist{{end}}</h2>
|
||||||
|
{{if and .UseAuth (eq .Meta.U.Group "anon")}}
|
||||||
|
<p>{{block "empty no rights" .}}You are not authorized to create new hyphae. Here is what you can do:{{end}}</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/login">{{block "empty log in" .}}Log in to your account, if you have one{{end}}</a></li>
|
||||||
|
{{if .AllowRegistration}}<li><a href="/register">{{block "empty register" .}}Register a new account{{end}}</a></li>{{end}}
|
||||||
|
</ul>
|
||||||
|
{{else}}
|
||||||
|
<div class="non-existent-hypha__ways">
|
||||||
|
<section class="non-existent-hypha__way">
|
||||||
|
<h3 class="non-existent-hypha__subtitle">📝 {{block "write a text" .}}Write a text{{end}}</h3>
|
||||||
|
<p>{{block "write a text tip" .}}Write a note, a diary, an article, a story or anything textual using <a href="/help/en/mycomarkup" class="shy-link">Mycomarkup</a>. Full history of edits to the document will be saved.{{end}}</p>
|
||||||
|
<p>{{block "write a text writing conventions" .}}Make sure to follow this wiki's writing conventions if there are any.{{end}}</p>
|
||||||
|
<a class="btn btn_accent stick-to-bottom" href="/edit/{{.HyphaName}}">{{block "write a text btn" .}}Create{{end}}</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="non-existent-hypha__way">
|
||||||
|
<h3 class="non-existent-hypha__subtitle">🖼 {{block "upload a media" .}}Upload a media{{end}}</h3>
|
||||||
|
<p>{{block "upload a media tip" .}}Upload a picture, a video or an audio. Most common formats can be accessed from the browser, others can be only downloaded afterwards. You can write a description for the media later.{{end}}</p>
|
||||||
|
<form action="/upload-binary/{{.HyphaName}}"
|
||||||
|
method="post" enctype="multipart/form-data"
|
||||||
|
class="upload-binary">
|
||||||
|
<input type="file" id="upload-binary__input" name="binary">
|
||||||
|
<button type="submit" class="btn stick-to-bottom" value="Upload">{{block "upload a media btn" .}}Upload{{end}}</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</section>
|
||||||
|
{{end}}
|
||||||
@ -13,8 +13,8 @@
|
|||||||
<input type="checkbox" id="recursive" name="recursive" value="true" checked/>
|
<input type="checkbox" id="recursive" name="recursive" value="true" checked/>
|
||||||
<label for="recursive">{{block "rename recursively" .}}Rename subhyphae too{{end}}</label>
|
<label for="recursive">{{block "rename recursively" .}}Rename subhyphae too{{end}}</label>
|
||||||
<br>
|
<br>
|
||||||
<input type="checkbox" id="redirection" name="redirection" value="true" {{if .LeaveRedirectionDefault}}checked{{end}}/>
|
<input type="checkbox" id="redirection" name="redirection" value="true" checked/>
|
||||||
<label for="redirection">{{block "leave redirection" .}}Leave redirection{{end}}</label>
|
<label for="redirection">{{block "leave redirections" .}}Leave redirections{{end}}</label>
|
||||||
|
|
||||||
<p>{{block "rename tip" .}}Rename carefully. <a href="/help/en/rename">Documentation.</a>{{end}}</p>
|
<p>{{block "rename tip" .}}Rename carefully. <a href="/help/en/rename">Documentation.</a>{{end}}</p>
|
||||||
<button type="submit" value="Confirm" class="btn">
|
<button type="submit" value="Confirm" class="btn">
|
||||||
|
|||||||
@ -1,36 +0,0 @@
|
|||||||
package shroom
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/user"
|
|
||||||
)
|
|
||||||
|
|
||||||
func rejectRenameLog(h hyphae.Hypha, u *user.User, errmsg string) {
|
|
||||||
slog.Info("Reject rename",
|
|
||||||
"hyphaName", h.CanonicalName(),
|
|
||||||
"username", u.Name,
|
|
||||||
"errmsg", errmsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func rejectRemoveMediaLog(h hyphae.Hypha, u *user.User, errmsg string) {
|
|
||||||
slog.Info("Reject remove media",
|
|
||||||
"hyphaName", h.CanonicalName(),
|
|
||||||
"username", u.Name,
|
|
||||||
"errmsg", errmsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func rejectEditLog(h hyphae.Hypha, u *user.User, errmsg string) {
|
|
||||||
slog.Info("Reject edit",
|
|
||||||
"hyphaName", h.CanonicalName(),
|
|
||||||
"username", u.Name,
|
|
||||||
"errmsg", errmsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func rejectUploadMediaLog(h hyphae.Hypha, u *user.User, errmsg string) {
|
|
||||||
slog.Info("Reject upload media",
|
|
||||||
"hyphaName", h.CanonicalName(),
|
|
||||||
"username", u.Name,
|
|
||||||
"errmsg", errmsg)
|
|
||||||
}
|
|
||||||
@ -1,130 +0,0 @@
|
|||||||
package tree
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"html/template"
|
|
||||||
"io"
|
|
||||||
"path"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tree returns the subhypha matrix as HTML and names of the next and previous hyphae (or empty strings).
|
|
||||||
func Tree(hyphaName string) (childrenHTML template.HTML, prev, next string) {
|
|
||||||
var (
|
|
||||||
root = child{hyphaName, true, make([]child, 0)}
|
|
||||||
descendantPrefix = hyphaName + "/"
|
|
||||||
parent = path.Dir(hyphaName) // Beware, it might be . and whatnot.
|
|
||||||
slashCount = strings.Count(hyphaName, "/")
|
|
||||||
)
|
|
||||||
for h := range hyphae.YieldExistingHyphae() {
|
|
||||||
name := h.CanonicalName()
|
|
||||||
if strings.HasPrefix(name, descendantPrefix) {
|
|
||||||
var subPath = strings.TrimPrefix(name, descendantPrefix)
|
|
||||||
addHyphaToChild(name, subPath, &root)
|
|
||||||
// A child is not a sibling, so we skip the rest.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skipping non-siblings.
|
|
||||||
if !(path.Dir(name) == parent && slashCount == strings.Count(name, "/")) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name < hyphaName) && (name > prev) {
|
|
||||||
prev = name
|
|
||||||
} else if (name > hyphaName) && (name < next || next == "") {
|
|
||||||
next = name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return subhyphaeMatrix(root.children), prev, next
|
|
||||||
}
|
|
||||||
|
|
||||||
type child struct {
|
|
||||||
name string
|
|
||||||
exists bool
|
|
||||||
children []child
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Subhyphae links are recursive. It may end up looking like that if drawn with
|
|
||||||
pseudographics:
|
|
||||||
╔══════════════╗
|
|
||||||
║Foo ║ The presented hyphae are ./foo and ./foo/bar
|
|
||||||
║╔════════════╗║
|
|
||||||
║║Bar ║║
|
|
||||||
║╚════════════╝║
|
|
||||||
╚══════════════╝
|
|
||||||
*/
|
|
||||||
func childHTML(c *child, w io.Writer) {
|
|
||||||
sort.Slice(c.children, func(i, j int) bool {
|
|
||||||
return c.children[i].name < c.children[j].name
|
|
||||||
})
|
|
||||||
|
|
||||||
_, _ = io.WriteString(w, "<li class=\"subhyphae__entry\">\n<a class=\"subhyphae__link")
|
|
||||||
if !c.exists {
|
|
||||||
_, _ = io.WriteString(w, " wikilink_new")
|
|
||||||
}
|
|
||||||
_, _ = io.WriteString(w, fmt.Sprintf(
|
|
||||||
"\" href=\"/hypha/%s\">%s</a>\n",
|
|
||||||
c.name,
|
|
||||||
util.BeautifulName(path.Base(c.name)),
|
|
||||||
))
|
|
||||||
|
|
||||||
if len(c.children) > 0 {
|
|
||||||
_, _ = io.WriteString(w, "<ul>\n")
|
|
||||||
for _, child := range c.children {
|
|
||||||
childHTML(&child, w)
|
|
||||||
}
|
|
||||||
_, _ = io.WriteString(w, "</ul>\n")
|
|
||||||
}
|
|
||||||
_, _ = io.WriteString(w, "</li>\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func addHyphaToChild(hyphaName, subPath string, child *child) {
|
|
||||||
// when hyphaName = "root/a/b", subPath = "a/b", and child.name = "root"
|
|
||||||
// addHyphaToChild("root/a/b", "b", child{"root/a"})
|
|
||||||
// when hyphaName = "root/a/b", subPath = "b", and child.name = "root/a"
|
|
||||||
// set .exists=true for "root/a/b", and create it if it isn't there already
|
|
||||||
var exists = !strings.Contains(subPath, "/")
|
|
||||||
if exists {
|
|
||||||
var subchild = findOrCreateSubchild(subPath, child)
|
|
||||||
subchild.exists = true
|
|
||||||
} else {
|
|
||||||
var (
|
|
||||||
firstSlash = strings.IndexRune(subPath, '/')
|
|
||||||
firstDir = subPath[:firstSlash]
|
|
||||||
restOfPath = subPath[firstSlash+1:]
|
|
||||||
subchild = findOrCreateSubchild(firstDir, child)
|
|
||||||
)
|
|
||||||
addHyphaToChild(hyphaName, restOfPath, subchild)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func findOrCreateSubchild(name string, baseChild *child) *child {
|
|
||||||
// when name = "a", and baseChild.name = "root"
|
|
||||||
// if baseChild.children contains "root/a", return it
|
|
||||||
// else create it and return that
|
|
||||||
var fullName = baseChild.name + "/" + name
|
|
||||||
for i := range baseChild.children {
|
|
||||||
if baseChild.children[i].name == fullName {
|
|
||||||
return &baseChild.children[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
baseChild.children = append(baseChild.children, child{fullName, false, make([]child, 0)})
|
|
||||||
return &baseChild.children[len(baseChild.children)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
func subhyphaeMatrix(children []child) template.HTML {
|
|
||||||
sort.Slice(children, func(i, j int) bool {
|
|
||||||
return children[i].name < children[j].name
|
|
||||||
})
|
|
||||||
var buf strings.Builder
|
|
||||||
for _, child := range children {
|
|
||||||
childHTML(&child, &buf)
|
|
||||||
}
|
|
||||||
return template.HTML(buf.String())
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
package version
|
|
||||||
|
|
||||||
import (
|
|
||||||
"regexp"
|
|
||||||
"runtime/debug"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/help"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Long is the full version string, including VCS information, that looks like
|
|
||||||
// x.y.z+hash-dirty.
|
|
||||||
var Long string
|
|
||||||
|
|
||||||
// Short is the human-friendly x.y.z part of the long version string.
|
|
||||||
var Short string
|
|
||||||
|
|
||||||
var versionRegexp = regexp.MustCompile(`This is documentation for Mycorrhiza Wiki (.*)\. `)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
if b, err := help.Get("en"); err == nil {
|
|
||||||
matches := versionRegexp.FindSubmatch(b)
|
|
||||||
if matches != nil {
|
|
||||||
Short = string(matches[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Long = Short
|
|
||||||
info, ok := debug.ReadBuildInfo()
|
|
||||||
if ok {
|
|
||||||
for _, setting := range info.Settings {
|
|
||||||
if setting.Key == "vcs.revision" {
|
|
||||||
val := setting.Value
|
|
||||||
if len(val) > 7 {
|
|
||||||
val = val[:7]
|
|
||||||
}
|
|
||||||
Long += "+" + val
|
|
||||||
} else if setting.Key == "vcs.modified" {
|
|
||||||
modified, err := strconv.ParseBool(setting.Value)
|
|
||||||
if err == nil && modified {
|
|
||||||
Long += "-dirty"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -4,62 +4,29 @@ package interwiki
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"log/slog"
|
"github.com/bouncepaw/mycomarkup/v5/options"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/util"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/files"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/util"
|
|
||||||
|
|
||||||
"git.sr.ht/~bouncepaw/mycomarkup/v5/options"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() error {
|
func Init() {
|
||||||
record, err := readInterwiki()
|
var (
|
||||||
|
record, err = readInterwiki()
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to read interwiki", "err", err)
|
log.Fatalln(err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, wiki := range record {
|
for _, wiki := range record {
|
||||||
wiki := wiki // This line is required
|
wiki := wiki // This line is required
|
||||||
if err := wiki.canonize(); err != nil {
|
wiki.canonize()
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := addEntry(&wiki); err != nil {
|
if err := addEntry(&wiki); err != nil {
|
||||||
slog.Error("Failed to add interwiki entry", "err", err)
|
log.Fatalln(err.Error())
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log.Printf("Loaded %d interwiki entries\n", len(listOfEntries))
|
||||||
slog.Info("Indexed interwiki map", "n", len(listOfEntries))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func dropEmptyStrings(ss []string) (clean []string) {
|
|
||||||
for _, s := range ss {
|
|
||||||
if s != "" {
|
|
||||||
clean = append(clean, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return clean
|
|
||||||
}
|
|
||||||
|
|
||||||
// difference returns the elements in `a` that aren't in `b`.
|
|
||||||
// Taken from https://stackoverflow.com/a/45428032
|
|
||||||
// CC BY-SA 4.0, no changes made
|
|
||||||
func difference(a, b []string) []string {
|
|
||||||
mb := make(map[string]struct{}, len(b))
|
|
||||||
for _, x := range b {
|
|
||||||
mb[x] = struct{}{}
|
|
||||||
}
|
|
||||||
var diff []string
|
|
||||||
for _, x := range a {
|
|
||||||
if _, found := mb[x]; !found {
|
|
||||||
diff = append(diff, x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return diff
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func areNamesFree(names []string) (bool, string) {
|
func areNamesFree(names []string) (bool, string) {
|
||||||
@ -73,68 +40,30 @@ func areNamesFree(names []string) (bool, string) {
|
|||||||
|
|
||||||
var mutex sync.Mutex
|
var mutex sync.Mutex
|
||||||
|
|
||||||
func replaceEntry(oldWiki *Wiki, newWiki *Wiki) error {
|
|
||||||
diff := difference(
|
|
||||||
append(newWiki.Aliases, newWiki.Name),
|
|
||||||
append(oldWiki.Aliases, oldWiki.Name),
|
|
||||||
)
|
|
||||||
if ok, name := areNamesFree(diff); !ok {
|
|
||||||
return errors.New(name)
|
|
||||||
}
|
|
||||||
deleteEntry(oldWiki)
|
|
||||||
return addEntry(newWiki)
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteEntry(wiki *Wiki) {
|
|
||||||
mutex.Lock()
|
|
||||||
defer mutex.Unlock()
|
|
||||||
|
|
||||||
// I'm being fancy here. Come on, the code here is already a mess.
|
|
||||||
// Let me have some fun.
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
wg.Add(2)
|
|
||||||
go func() {
|
|
||||||
names := append(wiki.Aliases, wiki.Name)
|
|
||||||
for _, name := range names {
|
|
||||||
name := name // I guess we need that
|
|
||||||
delete(entriesByName, name)
|
|
||||||
}
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for i, w := range listOfEntries {
|
|
||||||
i, w := i, w
|
|
||||||
if w.Name == wiki.Name {
|
|
||||||
// Drop ith element.
|
|
||||||
listOfEntries[i] = listOfEntries[len(listOfEntries)-1]
|
|
||||||
listOfEntries = listOfEntries[:len(listOfEntries)-1]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: There is something clearly wrong with error-returning in this function.
|
|
||||||
func addEntry(wiki *Wiki) error {
|
func addEntry(wiki *Wiki) error {
|
||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
defer mutex.Unlock()
|
defer mutex.Unlock()
|
||||||
wiki.Aliases = dropEmptyStrings(wiki.Aliases)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
names = append(wiki.Aliases, wiki.Name)
|
// non-empty names only
|
||||||
|
names = func(names []string) []string {
|
||||||
|
var result []string
|
||||||
|
for _, name := range names {
|
||||||
|
if name != "" {
|
||||||
|
result = append(result, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}(append(wiki.Aliases, wiki.Name))
|
||||||
ok, name = areNamesFree(names)
|
ok, name = areNamesFree(names)
|
||||||
)
|
)
|
||||||
switch {
|
if !ok {
|
||||||
case !ok:
|
log.Printf("There are multiple uses of the same name ‘%s’\n", name)
|
||||||
slog.Error("There are multiple uses of the same name", "name", name)
|
|
||||||
return errors.New(name)
|
return errors.New(name)
|
||||||
case len(names) == 0:
|
}
|
||||||
slog.Error("No names passed for a new interwiki entry")
|
if len(names) == 0 {
|
||||||
|
log.Println("No names passed for a new interwiki entry")
|
||||||
|
// There is something clearly wrong with error-returning in this function.
|
||||||
return errors.New("")
|
return errors.New("")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,13 +112,10 @@ func readInterwiki() ([]Wiki, error) {
|
|||||||
func saveInterwikiJson() {
|
func saveInterwikiJson() {
|
||||||
// Trust me, wiki crashing when an admin takes an administrative action totally makes sense.
|
// Trust me, wiki crashing when an admin takes an administrative action totally makes sense.
|
||||||
if data, err := json.MarshalIndent(listOfEntries, "", "\t"); err != nil {
|
if data, err := json.MarshalIndent(listOfEntries, "", "\t"); err != nil {
|
||||||
slog.Error("Failed to marshal interwiki entries", "err", err)
|
log.Fatalln(err)
|
||||||
os.Exit(1)
|
|
||||||
} else if err = os.WriteFile(files.InterwikiJSON(), data, 0666); err != nil {
|
} else if err = os.WriteFile(files.InterwikiJSON(), data, 0666); err != nil {
|
||||||
slog.Error("Failed to write interwiki.json", "err", err)
|
log.Fatalln(err)
|
||||||
os.Exit(1)
|
} else {
|
||||||
|
log.Println("Saved interwiki.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("Saved interwiki.json")
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,6 @@
|
|||||||
{{define "aliases (,)"}}Aliases (separated by commas):{{end}}
|
{{define "aliases (,)"}}Aliases (separated by commas):{{end}}
|
||||||
{{define "engine"}}Engine:{{end}}
|
{{define "engine"}}Engine:{{end}}
|
||||||
{{define "engine/mycorrhiza"}}Mycorrhiza{{end}}
|
{{define "engine/mycorrhiza"}}Mycorrhiza{{end}}
|
||||||
{{define "engine/betula"}}Betula{{end}}
|
|
||||||
{{define "engine/agora"}}Agora{{end}}
|
|
||||||
{{define "engine/generic"}}Generic (any website){{end}}
|
{{define "engine/generic"}}Generic (any website){{end}}
|
||||||
{{define "url"}}URL{{end}}
|
{{define "url"}}URL{{end}}
|
||||||
{{define "link href format"}}Link href attribute format string:{{end}}
|
{{define "link href format"}}Link href attribute format string:{{end}}
|
||||||
@ -55,7 +53,7 @@
|
|||||||
<form method="post" action="/interwiki/modify-entry/{{.Name}}">
|
<form method="post" action="/interwiki/modify-entry/{{.Name}}">
|
||||||
<p>
|
<p>
|
||||||
<label for="name{{$i}}" class="required-field">{{template "name"}}</label>
|
<label for="name{{$i}}" class="required-field">{{template "name"}}</label>
|
||||||
<input type="text" id="name{{$i}}" name="name" required
|
<input type="text" id="name" name="name{{$i}}" required
|
||||||
value="{{.Name}}">
|
value="{{.Name}}">
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
@ -72,8 +70,7 @@
|
|||||||
<label for="engine{{$i}}" class="required-field">{{template "engine"}}</label>
|
<label for="engine{{$i}}" class="required-field">{{template "engine"}}</label>
|
||||||
<select name="engine" id="engine{{$i}}" required>
|
<select name="engine" id="engine{{$i}}" required>
|
||||||
<option value="mycorrhiza" {{if eq .Engine "mycorrhiza"}}selected{{end}}>{{template "engine/mycorrhiza"}} 🍄</option>
|
<option value="mycorrhiza" {{if eq .Engine "mycorrhiza"}}selected{{end}}>{{template "engine/mycorrhiza"}} 🍄</option>
|
||||||
<option value="betula" {{if eq .Engine "betula"}}selected{{end}}>{{template "engine/betula"}} 🌳</option>
|
<option value="agora" {{if eq .Engine "agora"}}selected{{end}}>Agora ἀ</option>
|
||||||
<option value="agora" {{if eq .Engine "agora"}}selected{{end}}>{{template "engine/agora"}} ἀ</option>
|
|
||||||
<option value="generic" {{if eq .Engine "generic"}}selected{{end}}>{{template "engine/generic"}}</option>
|
<option value="generic" {{if eq .Engine "generic"}}selected{{end}}>{{template "engine/generic"}}</option>
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
@ -112,8 +109,7 @@
|
|||||||
<label for="engine" class="required-field">{{template "engine"}}</label>
|
<label for="engine" class="required-field">{{template "engine"}}</label>
|
||||||
<select name="engine" id="engine" required>
|
<select name="engine" id="engine" required>
|
||||||
<option value="mycorrhiza">{{template "engine/mycorrhiza"}} 🍄</option>
|
<option value="mycorrhiza">{{template "engine/mycorrhiza"}} 🍄</option>
|
||||||
<option value="betula">{{template "engine/betula"}} 🌳</option>
|
<option value="agora">Agora ἀ</option>
|
||||||
<option value="agora">{{template "engine/agora"}} ἀ</option>
|
|
||||||
<option value="generic">{{template "engine/generic"}}</option>
|
<option value="generic">{{template "engine/generic"}}</option>
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
@ -127,7 +123,7 @@
|
|||||||
<input type="url" id="img-src-format" name="img-src-format"
|
<input type="url" id="img-src-format" name="img-src-format"
|
||||||
placeholder="https://wiki.example.org/media/{NAME}">
|
placeholder="https://wiki.example.org/media/{NAME}">
|
||||||
</p>
|
</p>
|
||||||
<input type="submit" class="btn" value="Add entry">
|
<input type="submit" class="btn">
|
||||||
</form>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<main class="main-width">
|
<main class="main-width">
|
||||||
<h1>{{block "heading" .}}Name taken{{end}}</h1>
|
<h1>{{block "heading" .}}Name taken{{end}}</h1>
|
||||||
<p>{{block "tip" .TakenName}}Name <kbd>{{.}}</kbd> is already taken, please choose a different one.{{end}}</p>
|
<p>{{block "tip" .TakenName}}Name <kbd>{{.}}</kbd> is already taken, please choose a different one.{{end}}</p>
|
||||||
<form method="post" action="/interwiki/{{.Action}}">
|
<form method="post" action="/interwiki/add-entry">
|
||||||
<p>
|
<p>
|
||||||
<label for="name" class="required-field">Name:</label>
|
<label for="name" class="required-field">Name:</label>
|
||||||
<input type="text" id="name" name="name" required
|
<input type="text" id="name" name="name" required
|
||||||
@ -22,7 +22,6 @@
|
|||||||
<label for="engine" class="required-field">Engine:</label>
|
<label for="engine" class="required-field">Engine:</label>
|
||||||
<select name="engine" id="engine" required>
|
<select name="engine" id="engine" required>
|
||||||
<option value="mycorrhiza" {{if eq .Engine "mycorrhiza"}}selected{{end}}>Mycorrhiza 🍄</option>
|
<option value="mycorrhiza" {{if eq .Engine "mycorrhiza"}}selected{{end}}>Mycorrhiza 🍄</option>
|
||||||
<option value="betula" {{if eq .Engine "betula"}}selected{{end}}>Betula 🌳</option>
|
|
||||||
<option value="agora" {{if eq .Engine "agora"}}selected{{end}}>Agora ἀ</option>
|
<option value="agora" {{if eq .Engine "agora"}}selected{{end}}>Agora ἀ</option>
|
||||||
<option value="generic" {{if eq .Engine "generic"}}selected{{end}}>Generic (any website)</option>
|
<option value="generic" {{if eq .Engine "generic"}}selected{{end}}>Generic (any website)</option>
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@ -2,13 +2,10 @@ package interwiki
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"log/slog"
|
"github.com/bouncepaw/mycorrhiza/viewutil"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/web/viewutil"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -21,8 +18,6 @@ var (
|
|||||||
{{define "aliases (,)"}}Псевдонимы (разделённые запятыми):{{end}}
|
{{define "aliases (,)"}}Псевдонимы (разделённые запятыми):{{end}}
|
||||||
{{define "engine"}}Движок:{{end}}
|
{{define "engine"}}Движок:{{end}}
|
||||||
{{define "engine/mycorrhiza"}}Микориза{{end}}
|
{{define "engine/mycorrhiza"}}Микориза{{end}}
|
||||||
{{define "engine/betula"}}Бетула{{end}}
|
|
||||||
{{define "engine/agora"}}Агора{{end}}
|
|
||||||
{{define "engine/generic"}}Любой сайт{{end}}
|
{{define "engine/generic"}}Любой сайт{{end}}
|
||||||
{{define "link href format"}}Строка форматирования атрибута href ссылки:{{end}}
|
{{define "link href format"}}Строка форматирования атрибута href ссылки:{{end}}
|
||||||
{{define "img src format"}}Строка форматирования атрибута src изображения:{{end}}
|
{{define "img src format"}}Строка форматирования атрибута src изображения:{{end}}
|
||||||
@ -40,10 +35,9 @@ func InitHandlers(rtr *mux.Router) {
|
|||||||
chainNameTaken = viewutil.CopyEnRuWith(fs, "view_name_taken.html", ruTranslation)
|
chainNameTaken = viewutil.CopyEnRuWith(fs, "view_name_taken.html", ruTranslation)
|
||||||
rtr.HandleFunc("/interwiki", handlerInterwiki)
|
rtr.HandleFunc("/interwiki", handlerInterwiki)
|
||||||
rtr.HandleFunc("/interwiki/add-entry", handlerAddEntry).Methods(http.MethodPost)
|
rtr.HandleFunc("/interwiki/add-entry", handlerAddEntry).Methods(http.MethodPost)
|
||||||
rtr.HandleFunc("/interwiki/modify-entry/{target}", handlerModifyEntry).Methods(http.MethodPost)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readInterwikiEntryFromRequest(rq *http.Request) Wiki {
|
func handlerAddEntry(w http.ResponseWriter, rq *http.Request) {
|
||||||
wiki := Wiki{
|
wiki := Wiki{
|
||||||
Name: rq.PostFormValue("name"),
|
Name: rq.PostFormValue("name"),
|
||||||
Aliases: strings.Split(rq.PostFormValue("aliases"), ","),
|
Aliases: strings.Split(rq.PostFormValue("aliases"), ","),
|
||||||
@ -53,43 +47,8 @@ func readInterwikiEntryFromRequest(rq *http.Request) Wiki {
|
|||||||
Engine: WikiEngine(rq.PostFormValue("engine")),
|
Engine: WikiEngine(rq.PostFormValue("engine")),
|
||||||
}
|
}
|
||||||
wiki.canonize()
|
wiki.canonize()
|
||||||
return wiki
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerModifyEntry(w http.ResponseWriter, rq *http.Request) {
|
|
||||||
var (
|
|
||||||
oldData *Wiki
|
|
||||||
ok bool
|
|
||||||
name = mux.Vars(rq)["target"]
|
|
||||||
newData = readInterwikiEntryFromRequest(rq)
|
|
||||||
)
|
|
||||||
|
|
||||||
if oldData, ok = entriesByName[name]; !ok {
|
|
||||||
slog.Info("Could not modify entry",
|
|
||||||
"name", name,
|
|
||||||
"reason", "does not exist")
|
|
||||||
viewutil.HandlerNotFound(w, rq)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := replaceEntry(oldData, &newData); err != nil {
|
|
||||||
slog.Info("Could not modify entry",
|
|
||||||
"name", name,
|
|
||||||
"reason", "one of the proposed aliases or the name is taken",
|
|
||||||
"err", err)
|
|
||||||
viewNameTaken(viewutil.MetaFrom(w, rq), oldData, err.Error(), "modify-entry/"+name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
saveInterwikiJson()
|
|
||||||
slog.Info("Modified entry", "name", name)
|
|
||||||
http.Redirect(w, rq, "/interwiki", http.StatusSeeOther)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerAddEntry(w http.ResponseWriter, rq *http.Request) {
|
|
||||||
wiki := readInterwikiEntryFromRequest(rq)
|
|
||||||
if err := addEntry(&wiki); err != nil {
|
if err := addEntry(&wiki); err != nil {
|
||||||
viewNameTaken(viewutil.MetaFrom(w, rq), &wiki, err.Error(), "add-entry")
|
viewNameTaken(viewutil.MetaFrom(w, rq), &wiki, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
saveInterwikiJson()
|
saveInterwikiJson()
|
||||||
@ -100,15 +59,13 @@ type nameTakenData struct {
|
|||||||
*viewutil.BaseData
|
*viewutil.BaseData
|
||||||
*Wiki
|
*Wiki
|
||||||
TakenName string
|
TakenName string
|
||||||
Action string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func viewNameTaken(meta viewutil.Meta, wiki *Wiki, takenName, action string) {
|
func viewNameTaken(meta viewutil.Meta, wiki *Wiki, takenName string) {
|
||||||
viewutil.ExecutePage(meta, chainNameTaken, nameTakenData{
|
viewutil.ExecutePage(meta, chainNameTaken, nameTakenData{
|
||||||
BaseData: &viewutil.BaseData{},
|
BaseData: &viewutil.BaseData{},
|
||||||
Wiki: wiki,
|
Wiki: wiki,
|
||||||
TakenName: takenName,
|
TakenName: takenName,
|
||||||
Action: action,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
package interwiki
|
package interwiki
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/util"
|
"github.com/bouncepaw/mycorrhiza/util"
|
||||||
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WikiEngine is an enumeration of supported interwiki targets.
|
// WikiEngine is an enumeration of supported interwiki targets.
|
||||||
@ -13,7 +11,6 @@ type WikiEngine string
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
Mycorrhiza WikiEngine = "mycorrhiza"
|
Mycorrhiza WikiEngine = "mycorrhiza"
|
||||||
Betula WikiEngine = "betula"
|
|
||||||
Agora WikiEngine = "agora"
|
Agora WikiEngine = "agora"
|
||||||
// Generic is any website.
|
// Generic is any website.
|
||||||
Generic WikiEngine = "generic"
|
Generic WikiEngine = "generic"
|
||||||
@ -21,7 +18,7 @@ const (
|
|||||||
|
|
||||||
func (we WikiEngine) Valid() bool {
|
func (we WikiEngine) Valid() bool {
|
||||||
switch we {
|
switch we {
|
||||||
case Mycorrhiza, Betula, Agora, Generic:
|
case Mycorrhiza, Agora, Generic:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -49,20 +46,14 @@ type Wiki struct {
|
|||||||
Engine WikiEngine `json:"engine"`
|
Engine WikiEngine `json:"engine"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Wiki) canonize() error {
|
func (w *Wiki) canonize() {
|
||||||
switch {
|
switch {
|
||||||
case w.Name == "":
|
case w.Name == "":
|
||||||
slog.Error("A site in the interwiki map has no name")
|
log.Fatalln("Cannot have a wiki in the interwiki map with no name")
|
||||||
return errors.New("site with no name")
|
|
||||||
case w.URL == "":
|
case w.URL == "":
|
||||||
slog.Error("Site in the interwiki map has no URL", "name", w.Name)
|
log.Fatalf("Wiki ‘%s’ has no URL\n", w.Name)
|
||||||
return errors.New("site with no URL")
|
|
||||||
case !w.Engine.Valid():
|
case !w.Engine.Valid():
|
||||||
slog.Error("Site in the interwiki map has an unknown engine",
|
log.Fatalf("Unknown engine ‘%s’ for wiki ‘%s’\n", w.Engine, w.Name)
|
||||||
"siteName", w.Name,
|
|
||||||
"engine", w.Engine,
|
|
||||||
)
|
|
||||||
return errors.New("unknown engine")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Name = util.CanonicalName(w.Name)
|
w.Name = util.CanonicalName(w.Name)
|
||||||
@ -74,8 +65,6 @@ func (w *Wiki) canonize() error {
|
|||||||
switch w.Engine {
|
switch w.Engine {
|
||||||
case Mycorrhiza:
|
case Mycorrhiza:
|
||||||
w.LinkHrefFormat = fmt.Sprintf("%s/hypha/{NAME}", w.URL)
|
w.LinkHrefFormat = fmt.Sprintf("%s/hypha/{NAME}", w.URL)
|
||||||
case Betula:
|
|
||||||
w.LinkHrefFormat = fmt.Sprintf("%s/{BETULA-NAME}", w.URL)
|
|
||||||
case Agora:
|
case Agora:
|
||||||
w.LinkHrefFormat = fmt.Sprintf("%s/node/{NAME}", w.URL)
|
w.LinkHrefFormat = fmt.Sprintf("%s/node/{NAME}", w.URL)
|
||||||
default:
|
default:
|
||||||
@ -91,6 +80,4 @@ func (w *Wiki) canonize() error {
|
|||||||
w.ImgSrcFormat = fmt.Sprintf("%s/{NAME}", w.URL)
|
w.ImgSrcFormat = fmt.Sprintf("%s/{NAME}", w.URL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
23
l18n/en/admin.json
Normal file
23
l18n/en/admin.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"users_title": "Manage users",
|
||||||
|
"users_create": "Create a new user",
|
||||||
|
"users_reindex": "Reindex users",
|
||||||
|
"users_name": "Name",
|
||||||
|
"users_password": "Password",
|
||||||
|
"users_group": "Group",
|
||||||
|
"users_registered": "Registered at",
|
||||||
|
"users_actions": "Actions",
|
||||||
|
"users_notime": "unknown",
|
||||||
|
"users_edit": "Edit",
|
||||||
|
|
||||||
|
"user_title": "User %s",
|
||||||
|
"user_group_heading": "Change group",
|
||||||
|
"user_update": "Update",
|
||||||
|
"user_delete_heading": "Delete user",
|
||||||
|
"user_delete_tip": "Remove the user from the database. Changes made by the user will be preserved. It will be possible to take this username later.",
|
||||||
|
"user_delete_warn": "Are you sure you want to delete {{.name}} from the database? This action is irreversible.",
|
||||||
|
"user_delete": "Delete",
|
||||||
|
|
||||||
|
"newuser_title": "New user",
|
||||||
|
"newuser_create": "Create"
|
||||||
|
}
|
||||||
@ -3,29 +3,33 @@
|
|||||||
"password": "Password",
|
"password": "Password",
|
||||||
|
|
||||||
"register_title": "Register",
|
"register_title": "Register",
|
||||||
"register_header": "",
|
"register_header": "Register on {{.name}}",
|
||||||
"register_button": "Register",
|
"register_button": "Register",
|
||||||
|
|
||||||
"logout_title": "",
|
"login_title": "Login",
|
||||||
|
"login_header": "Log in to {{.name}}",
|
||||||
|
"login_button": "Log in",
|
||||||
|
|
||||||
|
"logout_title": "Logout?",
|
||||||
"logout_header": "Log out?",
|
"logout_header": "Log out?",
|
||||||
"logout_button": "Confirm",
|
"logout_button": "Confirm",
|
||||||
"logout_anon": "",
|
"logout_anon": "You cannot log out because you are not logged in.",
|
||||||
|
|
||||||
"lock_title": "Locked",
|
"lock_title": "Locked",
|
||||||
|
|
||||||
"password_tip": "",
|
"password_tip": "The server stores your password in an encrypted form; even administrators cannot read it.",
|
||||||
"cookie_tip": "",
|
"cookie_tip": "By submitting this form you give this wiki a permission to store cookies in your browser. It lets the engine associate your edits with you. You will stay logged in until you log out.",
|
||||||
"telegram_tip": "",
|
"telegram_tip": "You can log in using Telegram. It only works if you have set your @username in Telegram and this username is free on this wiki.",
|
||||||
|
|
||||||
"noauth": "",
|
"noauth": "Authentication is disabled. You can make edits anonymously.",
|
||||||
"noregister": "Registrations are currently closed. Administrators can make an account for you by hand; contact them.",
|
"noregister": "Registrations are currently closed. Administrators can make an account for you by hand; contact them.",
|
||||||
|
|
||||||
"error_username": "",
|
"error_username": "Unknown username.",
|
||||||
"error_password": "",
|
"error_password": "Wrong password.",
|
||||||
"error_telegram": "",
|
"error_telegram": "Could not authorize using Telegram.",
|
||||||
|
|
||||||
"go_back": "Go back",
|
"go_back": "Go back",
|
||||||
"go_home": "",
|
"go_home": "Go home",
|
||||||
"go_login": "Go to the login page",
|
"go_login": "Go to the login page",
|
||||||
"try_again": "Try again"
|
"try_again": "Try again"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,18 @@
|
|||||||
"title_search": "Search by title",
|
"title_search": "Search by title",
|
||||||
"admin_panel": "Admin panel",
|
"admin_panel": "Admin panel",
|
||||||
|
|
||||||
|
"edit_link": "Edit text",
|
||||||
|
"logout_link": "Log out",
|
||||||
|
"history_link": "View history",
|
||||||
|
"rename_link": "Rename",
|
||||||
|
"delete_link": "Delete",
|
||||||
|
"text_link": "View markup",
|
||||||
|
"media_link": "Manage media",
|
||||||
|
"backlinks_link": "{{.n}} backlink%s",
|
||||||
|
"backlinks_link+one": "",
|
||||||
|
"backlinks_link+other": "s",
|
||||||
|
|
||||||
|
"sibling_hyphae": "Sibling hyphae",
|
||||||
"subhyphae": "Subhyphae",
|
"subhyphae": "Subhyphae",
|
||||||
|
|
||||||
"random_no_hyphae": "There are no hyphae",
|
"random_no_hyphae": "There are no hyphae",
|
||||||
@ -45,9 +57,9 @@
|
|||||||
|
|
||||||
"diff_title": "Diff of {{.name}} at {{.rev}}",
|
"diff_title": "Diff of {{.name}} at {{.rev}}",
|
||||||
|
|
||||||
"revision_title": "",
|
"revision_title": "{{.name}} at {{.rev}}",
|
||||||
"revision_warning": "",
|
"revision_warning": "Please note that viewing media is not supported in history for now.",
|
||||||
"revision_link": "",
|
"revision_link": "Get Mycomarkup source of this revision",
|
||||||
"revision_no_text": "This hypha had no text at this revision.",
|
"revision_no_text": "This hypha had no text at this revision.",
|
||||||
|
|
||||||
"about_title": "About {{.name}}",
|
"about_title": "About {{.name}}",
|
||||||
@ -64,6 +76,26 @@
|
|||||||
"media_noaudio": "Your browser does not support audio.",
|
"media_noaudio": "Your browser does not support audio.",
|
||||||
"media_noaudio_link": "Download audio",
|
"media_noaudio_link": "Download audio",
|
||||||
|
|
||||||
|
"media_title": "Media of {{.name}}",
|
||||||
|
"media_empty": "This hypha has no media, you can upload it here.",
|
||||||
|
"media_tip": "You can manage the hypha's media on this page.",
|
||||||
|
"media_what_is": "What is media?",
|
||||||
|
"media_upload": "Upload",
|
||||||
|
"media_stat": "Stat",
|
||||||
|
"media_stat_size": "File size:",
|
||||||
|
"media_size_value": "{{.n}} byte%s",
|
||||||
|
"media_size_value+one": "",
|
||||||
|
"media_size_value+other": "s",
|
||||||
|
"media_stat_mime": "MIME type:",
|
||||||
|
"media_include": "Include",
|
||||||
|
"media_include_tip": "This media is an image. To include it in a hypha, use a syntax like this:",
|
||||||
|
"media_new": "media",
|
||||||
|
"media_new_tip": "You can upload a new media. Please do not upload too big pictures unless you need to because may not want to wait for big pictures to load.",
|
||||||
|
"media_remove": "Remove media",
|
||||||
|
"media_remove_tip": "Please note that you don't have to remove media before uploading a new media.",
|
||||||
|
"media_remove_button": "Remove media",
|
||||||
|
|
||||||
|
"close_dialog": "Close this dialog",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"cancel": "Cancel"
|
"cancel": "Cancel"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log/slog"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@ -40,7 +40,6 @@ type Localizer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// locales is a filesystem containing all localization files.
|
// locales is a filesystem containing all localization files.
|
||||||
//
|
|
||||||
//go:embed en ru
|
//go:embed en ru
|
||||||
var locales embed.FS
|
var locales embed.FS
|
||||||
|
|
||||||
@ -78,7 +77,7 @@ func init() {
|
|||||||
|
|
||||||
var strings map[string]string
|
var strings map[string]string
|
||||||
if err := json.Unmarshal(contents, &strings); err != nil {
|
if err := json.Unmarshal(contents, &strings); err != nil {
|
||||||
slog.Error("Failed to unmarshal localization file", "path", path, "err", err)
|
log.Fatalf("error while parsing %s: %v", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, value := range strings {
|
for key, value := range strings {
|
||||||
|
|||||||
23
l18n/ru/admin.json
Normal file
23
l18n/ru/admin.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"users_title": "Менеджер пользователей",
|
||||||
|
"users_create": "Создать пользователя",
|
||||||
|
"users_reindex": "Переиндексировать пользователей",
|
||||||
|
"users_name": "Имя",
|
||||||
|
"users_password": "Пароль",
|
||||||
|
"users_group": "Группа",
|
||||||
|
"users_registered": "Время создания",
|
||||||
|
"users_actions": "Действия",
|
||||||
|
"users_notime": "неизвестно",
|
||||||
|
"users_edit": "Изменить",
|
||||||
|
|
||||||
|
"user_title": "Пользователь %s",
|
||||||
|
"user_group_heading": "Изменить группу",
|
||||||
|
"user_update": "Обновить",
|
||||||
|
"user_delete_heading": "Удалить пользователя",
|
||||||
|
"user_delete_tip": "Удаляет пользователя из базы данных. Правки пользователя будут сохранены. Имя пользователя освободится для повторной регистрации.",
|
||||||
|
"user_delete_warn": "Вы уверены, что хотите удалить {{.name}} из базы данных? Это действие нельзя отменить.",
|
||||||
|
"user_delete": "Удалить",
|
||||||
|
|
||||||
|
"newuser_title": "Новый пользователь",
|
||||||
|
"newuser_create": "Создать"
|
||||||
|
}
|
||||||
@ -2,31 +2,31 @@
|
|||||||
"username": "Логин",
|
"username": "Логин",
|
||||||
"password": "Пароль",
|
"password": "Пароль",
|
||||||
|
|
||||||
"register_title": "",
|
"register_title": "Регистрация",
|
||||||
"register_header": "",
|
"register_header": "Регистрация на «{{.name}}»",
|
||||||
"register_button": "",
|
"register_button": "Зарегистрироваться",
|
||||||
|
|
||||||
"login_title": "Вход",
|
"login_title": "Вход",
|
||||||
"login_header": "",
|
"login_header": "Вход в «{{.name}}»",
|
||||||
"login_button": "",
|
"login_button": "Войти",
|
||||||
|
|
||||||
"logout_title": "",
|
"logout_title": "Выйти?",
|
||||||
"logout_header": "?",
|
"logout_header": "Выйти?",
|
||||||
"logout_button": "Подтвердить",
|
"logout_button": "Подтвердить",
|
||||||
"logout_anon": "",
|
"logout_anon": "Вы не можете выйти, потому что ещё не вошли.",
|
||||||
|
|
||||||
"lock_title": "",
|
"lock_title": "Доступ закрыт",
|
||||||
|
|
||||||
"password_tip": "",
|
"password_tip": "Сервер хранит ваш пароль в зашифрованном виде, даже администраторы не смогут его прочесть.",
|
||||||
"cookie_tip": "",
|
"cookie_tip": "Отправляя эту форму, вы разрешаете вики хранить cookie в вашем браузере. Это позволит движку связывать ваши правки с вашей учётной записью. Вы будете авторизованы, пока не выйдете из учётной записи.",
|
||||||
"telegram_tip": "Вы можете войти с помощью Телеграм. Это сработает, если у вашего профиля есть @имя, и оно не занято в этой вики.",
|
"telegram_tip": "Вы можете войти с помощью Телеграм. Это сработает, если у вашего профиля есть @имя, и оно не занято в этой вики.",
|
||||||
|
|
||||||
"noauth": "",
|
"noauth": "Аутентификация отключена. Вы можете делать правки анонимно.",
|
||||||
"noregister": "Регистрация в текущее время недоступна. Администраторы могут вручную создать вам учётную запись, свяжитесь с ними.",
|
"noregister": "Регистрация в текущее время недоступна. Администраторы могут вручную создать вам учётную запись, свяжитесь с ними.",
|
||||||
|
|
||||||
"error_username": "",
|
"error_username": "Неизвестное имя пользователя.",
|
||||||
"error_password": "Неверный пароль.",
|
"error_password": "Неверный пароль.",
|
||||||
"error_telegram": "",
|
"error_telegram": "Не удалось авторизоваться через Телеграм.",
|
||||||
|
|
||||||
"go_back": "Назад",
|
"go_back": "Назад",
|
||||||
"go_home": "Домой",
|
"go_home": "Домой",
|
||||||
|
|||||||
@ -29,7 +29,7 @@
|
|||||||
"bullets": "Маркир. список",
|
"bullets": "Маркир. список",
|
||||||
"numbers": "Нумер. список",
|
"numbers": "Нумер. список",
|
||||||
|
|
||||||
"help": "{{.link}} о Микоразметке",
|
"help": "{{.link}} о микоразметке",
|
||||||
"help_link": "Подробнее",
|
"help_link": "Подробнее",
|
||||||
|
|
||||||
"selflink": "Ссылка на вас",
|
"selflink": "Ссылка на вас",
|
||||||
|
|||||||
@ -8,19 +8,19 @@
|
|||||||
"backlinks_heading": "Обратные ссылки на {{.hypha_link}}",
|
"backlinks_heading": "Обратные ссылки на {{.hypha_link}}",
|
||||||
"backlinks_desc": "Ниже перечислены гифы, на которых есть ссылка на эту гифу, трансклюзия этой гифы или эта гифа вставлена как изображение.",
|
"backlinks_desc": "Ниже перечислены гифы, на которых есть ссылка на эту гифу, трансклюзия этой гифы или эта гифа вставлена как изображение.",
|
||||||
|
|
||||||
"edit_link": "",
|
"edit_link": "Редактировать",
|
||||||
"logout_link": "",
|
"logout_link": "Выйти",
|
||||||
"history_link": "",
|
"history_link": "История",
|
||||||
"rename_link": "",
|
"rename_link": "Переименовать",
|
||||||
"delete_link": "",
|
"delete_link": "Удалить",
|
||||||
"text_link": "",
|
"text_link": "Посмотреть разметку",
|
||||||
"media_link": "",
|
"media_link": "Медиа",
|
||||||
"media_link_for_textual": "",
|
|
||||||
"backlinks_link": "{{.n}} %s сюда",
|
"backlinks_link": "{{.n}} %s сюда",
|
||||||
"backlinks_link+one": "ссылка",
|
"backlinks_link+one": "ссылка",
|
||||||
"backlinks_link+few": "ссылки",
|
"backlinks_link+few": "ссылки",
|
||||||
"backlinks_link+many": "ссылок",
|
"backlinks_link+many": "ссылок",
|
||||||
|
|
||||||
|
"sibling_hyphae": "Гифы-сиблинги",
|
||||||
"subhyphae": "Подгифы",
|
"subhyphae": "Подгифы",
|
||||||
|
|
||||||
"random_no_hyphae": "В этой вики нет гиф",
|
"random_no_hyphae": "В этой вики нет гиф",
|
||||||
@ -59,9 +59,9 @@
|
|||||||
"ask_really": "Вы действительно хотите {{.verb}} гифу «{{.name}}»?",
|
"ask_really": "Вы действительно хотите {{.verb}} гифу «{{.name}}»?",
|
||||||
"ask_remove_media_verb": "убрать медиа",
|
"ask_remove_media_verb": "убрать медиа",
|
||||||
|
|
||||||
"revision_title": "",
|
"revision_title": "{{.name}} из {{.rev}}",
|
||||||
"revision_warning": "",
|
"revision_warning": "Обратите внимание, просмотр медиа в истории пока что недоступен.",
|
||||||
"revision_link": "",
|
"revision_link": "Посмотреть код микоразметки для этой ревизии",
|
||||||
"revision_no_text": "В этой ревизии гифы не было текста.",
|
"revision_no_text": "В этой ревизии гифы не было текста.",
|
||||||
|
|
||||||
"about_title": "О {{.name}}",
|
"about_title": "О {{.name}}",
|
||||||
@ -78,6 +78,27 @@
|
|||||||
"media_noaudio": "Ваш браузер не поддерживает аудио.",
|
"media_noaudio": "Ваш браузер не поддерживает аудио.",
|
||||||
"media_noaudio_link": "Скачать аудио",
|
"media_noaudio_link": "Скачать аудио",
|
||||||
|
|
||||||
|
"media_title": "Медиа «{{.name}}»",
|
||||||
|
"media_empty": "Эта гифа не имеет медиа, здесь вы можете его загрузить.",
|
||||||
|
"media_tip": "На этой странице вы можете управлять медиа.",
|
||||||
|
"media_what_is": "Что такое медиа?",
|
||||||
|
"media_upload": "Загрузить",
|
||||||
|
"media_stat": "Свойства",
|
||||||
|
"media_stat_size": "Размер файла:",
|
||||||
|
"media_size_value": "{{.n}} %s",
|
||||||
|
"media_size_value+one": "байт",
|
||||||
|
"media_size_value+few": "байта",
|
||||||
|
"media_size_value+many": "байт",
|
||||||
|
"media_stat_mime": "MIME-тип:",
|
||||||
|
"media_include": "Добавление",
|
||||||
|
"media_include_tip": "Это медиа – изображение. Чтобы добавить его в текст гифы, используйте синтаксис ниже:",
|
||||||
|
"media_new": "Прикрепить",
|
||||||
|
"media_new_tip": "Вы можете загрузить новое медиа. Пожалуйста, не загружайте слишком большие изображения без необходимости, чтобы впоследствии не ждать её долгую загрузку.",
|
||||||
|
"media_remove": "Открепить",
|
||||||
|
"media_remove_tip": "Заметьте, чтобы заменить медиа, вам не нужно его перед этим откреплять.",
|
||||||
|
"media_remove_button": "Открепить",
|
||||||
|
|
||||||
|
"close_dialog": "Закрыть этот диалог",
|
||||||
"confirm": "Применить",
|
"confirm": "Применить",
|
||||||
"cancel": "Отмена"
|
"cancel": "Отмена"
|
||||||
}
|
}
|
||||||
|
|||||||
69
main.go
69
main.go
@ -1,79 +1,62 @@
|
|||||||
|
//go:generate go run github.com/valyala/quicktemplate/qtc -dir=views
|
||||||
|
//go:generate go run github.com/valyala/quicktemplate/qtc -dir=tree
|
||||||
|
//go:generate go run github.com/valyala/quicktemplate/qtc -dir=history
|
||||||
|
//go:generate go run github.com/valyala/quicktemplate/qtc -dir=mycoopts
|
||||||
// Command mycorrhiza is a program that runs a mycorrhiza wiki.
|
// Command mycorrhiza is a program that runs a mycorrhiza wiki.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
"github.com/bouncepaw/mycorrhiza/backlinks"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/categories"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/interwiki"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/migration"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/viewutil"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
"github.com/bouncepaw/mycorrhiza/history"
|
"github.com/bouncepaw/mycorrhiza/history"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/backlinks"
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/categories"
|
"github.com/bouncepaw/mycorrhiza/shroom"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/cfg"
|
"github.com/bouncepaw/mycorrhiza/static"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/files"
|
"github.com/bouncepaw/mycorrhiza/user"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/migration"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/shroom"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/user"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/version"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/interwiki"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/web"
|
"github.com/bouncepaw/mycorrhiza/web"
|
||||||
"github.com/bouncepaw/mycorrhiza/web/static"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/web/viewutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := parseCliArgs(); err != nil {
|
parseCliArgs()
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := files.PrepareWikiRoot(); err != nil {
|
if err := files.PrepareWikiRoot(); err != nil {
|
||||||
slog.Error("Failed to prepare wiki root", "err", err)
|
log.Fatal(err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cfg.ReadConfigFile(files.ConfigPath()); err != nil {
|
if err := cfg.ReadConfigFile(files.ConfigPath()); err != nil {
|
||||||
slog.Error("Failed to read config", "err", err)
|
log.Fatal(err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Println("Running Mycorrhiza Wiki 1.11.0")
|
||||||
if err := os.Chdir(files.HyphaeDir()); err != nil {
|
if err := os.Chdir(files.HyphaeDir()); err != nil {
|
||||||
slog.Error("Failed to chdir to hyphae dir",
|
log.Fatal(err)
|
||||||
"err", err, "hyphaeDir", files.HyphaeDir())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
slog.Info("Running Mycorrhiza Wiki",
|
log.Println("Wiki directory is", cfg.WikiDir)
|
||||||
"version", version.Short, "wikiDir", cfg.WikiDir)
|
|
||||||
|
|
||||||
// Init the subsystems:
|
// Init the subsystems:
|
||||||
// TODO: keep all crashes in main rather than somewhere there
|
|
||||||
viewutil.Init()
|
viewutil.Init()
|
||||||
hyphae.Index(files.HyphaeDir())
|
hyphae.Index(files.HyphaeDir())
|
||||||
backlinks.IndexBacklinks()
|
backlinks.IndexBacklinks()
|
||||||
go backlinks.RunBacklinksConveyor()
|
go backlinks.RunBacklinksConveyor()
|
||||||
user.InitUserDatabase()
|
user.InitUserDatabase()
|
||||||
if err := history.Start(); err != nil {
|
history.Start()
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
history.InitGitRepo()
|
history.InitGitRepo()
|
||||||
migration.MigrateRocketsMaybe()
|
migration.MigrateRocketsMaybe()
|
||||||
migration.MigrateHeadingsMaybe()
|
migration.MigrateHeadingsMaybe()
|
||||||
shroom.SetHeaderLinks()
|
shroom.SetHeaderLinks()
|
||||||
if err := categories.Init(); err != nil {
|
categories.Init()
|
||||||
os.Exit(1)
|
interwiki.Init()
|
||||||
}
|
|
||||||
if err := interwiki.Init(); err != nil {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static files:
|
// Static files:
|
||||||
static.InitFS(files.StaticFiles())
|
static.InitFS(files.StaticFiles())
|
||||||
|
|
||||||
if !user.HasAnyAdmins() {
|
serveHTTP(web.Handler())
|
||||||
slog.Error("Your wiki has no admin yet. Run Mycorrhiza with -create-admin <username> option to create an admin.")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := serveHTTP(web.Handler())
|
|
||||||
if err != nil {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
package migration
|
package migration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bouncepaw/mycomarkup/v5/tools"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log/slog"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/files"
|
|
||||||
|
|
||||||
"git.sr.ht/~bouncepaw/mycomarkup/v5/tools"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var headingMarkerPath string
|
var headingMarkerPath string
|
||||||
@ -31,8 +29,7 @@ func shouldMigrateHeadings() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to check if heading migration is needed", "err", err)
|
log.Fatalln("When checking if heading migration is needed:", err.Error())
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
return false
|
return false
|
||||||
@ -45,7 +42,6 @@ func createHeadingMarker() {
|
|||||||
0766,
|
0766,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to create heading migration marker", "err", err)
|
log.Fatalln(err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3,19 +3,18 @@
|
|||||||
// Migrations are meant to be removed couple of versions after being introduced.
|
// Migrations are meant to be removed couple of versions after being introduced.
|
||||||
//
|
//
|
||||||
// Available migrations:
|
// Available migrations:
|
||||||
// - Rocket links
|
// * Rocket links
|
||||||
// - Headings
|
// * Headings
|
||||||
package migration
|
package migration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bouncepaw/mycorrhiza/history"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/user"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/history"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/user"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func genericLineMigrator(
|
func genericLineMigrator(
|
||||||
@ -37,8 +36,7 @@ func genericLineMigrator(
|
|||||||
file, err := os.OpenFile(hypha.TextFilePath(), os.O_RDWR, 0766)
|
file, err := os.OpenFile(hypha.TextFilePath(), os.O_RDWR, 0766)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hop.WithErrAbort(err)
|
hop.WithErrAbort(err)
|
||||||
slog.Error("Failed to open text part file", "path", hypha.TextFilePath(), "err", err)
|
log.Fatal("Something went wrong when opening ", hypha.TextFilePath(), ": ", err.Error())
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf strings.Builder
|
var buf strings.Builder
|
||||||
@ -46,7 +44,7 @@ func genericLineMigrator(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
hop.WithErrAbort(err)
|
hop.WithErrAbort(err)
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
slog.Error("Failed to read text part file", "path", hypha.TextFilePath(), "err", err)
|
log.Fatal("Something went wrong when reading ", hypha.TextFilePath(), ": ", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -60,24 +58,21 @@ func genericLineMigrator(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
hop.WithErrAbort(err)
|
hop.WithErrAbort(err)
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
slog.Error("Failed to truncate text part file", "path", hypha.TextFilePath(), "err", err)
|
log.Fatal("Something went wrong when truncating ", hypha.TextFilePath(), ": ", err.Error())
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = file.Seek(0, 0)
|
_, err = file.Seek(0, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hop.WithErrAbort(err)
|
hop.WithErrAbort(err)
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
slog.Error("Failed to seek in text part file", "path", hypha.TextFilePath(), "err", err)
|
log.Fatal("Something went wrong when seeking in ", hypha.TextFilePath(), ": ", err.Error())
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = file.WriteString(newText)
|
_, err = file.WriteString(newText)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hop.WithErrAbort(err)
|
hop.WithErrAbort(err)
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
slog.Error("Failed to write to text part file", "path", hypha.TextFilePath(), "err", err)
|
log.Fatal("Something went wrong when writing to ", hypha.TextFilePath(), ": ", err.Error())
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
@ -89,8 +84,8 @@ func genericLineMigrator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if hop.WithFiles(mycoFiles...).Apply().HasErrors() {
|
if hop.WithFiles(mycoFiles...).Apply().HasErrors() {
|
||||||
slog.Error(commitErrorMessage + hop.FirstErrorText())
|
log.Fatal(commitErrorMessage, hop.FirstErrorText())
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("Migrated Mycomarkup documents", "n", len(mycoFiles))
|
log.Println("Migrated", len(mycoFiles), "Mycomarkup documents")
|
||||||
}
|
}
|
||||||
@ -1,13 +1,11 @@
|
|||||||
package migration
|
package migration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bouncepaw/mycomarkup/v5/tools"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log/slog"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/files"
|
|
||||||
|
|
||||||
"git.sr.ht/~bouncepaw/mycomarkup/v5/tools"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var rocketMarkerPath string
|
var rocketMarkerPath string
|
||||||
@ -35,8 +33,7 @@ func shouldMigrateRockets() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to check if rocket migration is needed", "err", err)
|
log.Fatalln("When checking if rocket migration is needed:", err.Error())
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
return false
|
return false
|
||||||
@ -49,7 +46,6 @@ func createRocketLinkMarker() {
|
|||||||
0766,
|
0766,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to create rocket link migration marker")
|
log.Fatalln(err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,33 +40,20 @@ func DataFromFilename(fullPath string) (name string, isText bool, skip bool) {
|
|||||||
|
|
||||||
var mapMime2Ext = map[string]string{
|
var mapMime2Ext = map[string]string{
|
||||||
"application/octet-stream": "bin",
|
"application/octet-stream": "bin",
|
||||||
|
|
||||||
"image/jpeg": "jpg",
|
"image/jpeg": "jpg",
|
||||||
"image/gif": "gif",
|
"image/gif": "gif",
|
||||||
"image/png": "png",
|
"image/png": "png",
|
||||||
"image/webp": "webp",
|
"image/webp": "webp",
|
||||||
"image/svg+xml": "svg",
|
"image/svg+xml": "svg",
|
||||||
"image/x-icon": "ico",
|
"image/x-icon": "ico",
|
||||||
|
|
||||||
"application/ogg": "ogg",
|
"application/ogg": "ogg",
|
||||||
"video/webm": "webm",
|
"video/webm": "webm",
|
||||||
"audio/mp3": "mp3",
|
"audio/mp3": "mp3",
|
||||||
"audio/mpeg": "mp3",
|
|
||||||
"audio/mpeg3": "mp3",
|
|
||||||
"video/mp4": "mp4",
|
"video/mp4": "mp4",
|
||||||
"audio/flac": "flac",
|
|
||||||
|
|
||||||
"audio/wav": "wav",
|
|
||||||
"audio/vnd.wav": "wav",
|
|
||||||
"audio/vnd.wave": "wav",
|
|
||||||
"audio/wave": "wav",
|
|
||||||
"audio/x-pn-wav": "wav",
|
|
||||||
"audio/x-wav": "wav",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var mapExt2Mime = map[string]string{
|
var mapExt2Mime = map[string]string{
|
||||||
".bin": "application/octet-stream",
|
".bin": "application/octet-stream",
|
||||||
|
|
||||||
".jpg": "image/jpeg",
|
".jpg": "image/jpeg",
|
||||||
".jpeg": "image/jpeg",
|
".jpeg": "image/jpeg",
|
||||||
".gif": "image/gif",
|
".gif": "image/gif",
|
||||||
@ -74,12 +61,8 @@ var mapExt2Mime = map[string]string{
|
|||||||
".webp": "image/webp",
|
".webp": "image/webp",
|
||||||
".svg": "image/svg+xml",
|
".svg": "image/svg+xml",
|
||||||
".ico": "image/x-icon",
|
".ico": "image/x-icon",
|
||||||
|
|
||||||
".ogg": "application/ogg",
|
".ogg": "application/ogg",
|
||||||
".webm": "video/webm",
|
".webm": "video/webm",
|
||||||
".mp3": "audio/mpeg",
|
".mp3": "audio/mp3",
|
||||||
".mp4": "video/mp4",
|
".mp4": "video/mp4",
|
||||||
".flac": "audio/flac",
|
|
||||||
|
|
||||||
"wav": "audio/wav",
|
|
||||||
}
|
}
|
||||||
@ -2,43 +2,39 @@
|
|||||||
package misc
|
package misc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bouncepaw/mycorrhiza/backlinks"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/l18n"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/shroom"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/static"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/user"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/util"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/views"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/viewutil"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/backlinks"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/cfg"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/files"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/shroom"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/user"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/l18n"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/util"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/web/static"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/web/viewutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitAssetHandlers(rtr *mux.Router) {
|
func InitHandlers(rtr *mux.Router) {
|
||||||
rtr.HandleFunc("/static/style.css", handlerStyle)
|
|
||||||
rtr.HandleFunc("/robots.txt", handlerRobotsTxt)
|
rtr.HandleFunc("/robots.txt", handlerRobotsTxt)
|
||||||
|
rtr.HandleFunc("/static/style.css", handlerStyle)
|
||||||
rtr.PathPrefix("/static/").
|
rtr.PathPrefix("/static/").
|
||||||
Handler(http.StripPrefix("/static/", http.FileServer(http.FS(static.FS))))
|
Handler(http.StripPrefix("/static/", http.FileServer(http.FS(static.FS))))
|
||||||
rtr.HandleFunc("/favicon.ico", func(w http.ResponseWriter, rq *http.Request) {
|
|
||||||
http.Redirect(w, rq, "/static/favicon.ico", http.StatusSeeOther)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func InitHandlers(rtr *mux.Router) {
|
|
||||||
rtr.HandleFunc("/list", handlerList)
|
rtr.HandleFunc("/list", handlerList)
|
||||||
rtr.HandleFunc("/reindex", handlerReindex)
|
rtr.HandleFunc("/reindex", handlerReindex)
|
||||||
rtr.HandleFunc("/update-header-links", handlerUpdateHeaderLinks)
|
rtr.HandleFunc("/update-header-links", handlerUpdateHeaderLinks)
|
||||||
rtr.HandleFunc("/random", handlerRandom)
|
rtr.HandleFunc("/random", handlerRandom)
|
||||||
rtr.HandleFunc("/about", handlerAbout)
|
rtr.HandleFunc("/about", handlerAbout)
|
||||||
|
rtr.HandleFunc("/favicon.ico", func(w http.ResponseWriter, rq *http.Request) {
|
||||||
|
http.Redirect(w, rq, "/static/favicon.ico", http.StatusSeeOther)
|
||||||
|
})
|
||||||
rtr.HandleFunc("/title-search/", handlerTitleSearch)
|
rtr.HandleFunc("/title-search/", handlerTitleSearch)
|
||||||
initViews()
|
initViews()
|
||||||
}
|
}
|
||||||
@ -73,11 +69,11 @@ func handlerReindex(w http.ResponseWriter, rq *http.Request) {
|
|||||||
if ok := user.CanProceed(rq, "reindex"); !ok {
|
if ok := user.CanProceed(rq, "reindex"); !ok {
|
||||||
var lc = l18n.FromRequest(rq)
|
var lc = l18n.FromRequest(rq)
|
||||||
viewutil.HttpErr(viewutil.MetaFrom(w, rq), http.StatusForbidden, cfg.HomeHypha, lc.Get("ui.reindex_no_rights"))
|
viewutil.HttpErr(viewutil.MetaFrom(w, rq), http.StatusForbidden, cfg.HomeHypha, lc.Get("ui.reindex_no_rights"))
|
||||||
slog.Info("No rights to reindex")
|
log.Println("Rejected", rq.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hyphae.ResetCount()
|
hyphae.ResetCount()
|
||||||
slog.Info("Reindexing hyphae", "hyphaeDir", files.HyphaeDir())
|
log.Println("Reindexing hyphae in", files.HyphaeDir())
|
||||||
hyphae.Index(files.HyphaeDir())
|
hyphae.Index(files.HyphaeDir())
|
||||||
backlinks.IndexBacklinks()
|
backlinks.IndexBacklinks()
|
||||||
http.Redirect(w, rq, "/", http.StatusSeeOther)
|
http.Redirect(w, rq, "/", http.StatusSeeOther)
|
||||||
@ -89,10 +85,9 @@ func handlerUpdateHeaderLinks(w http.ResponseWriter, rq *http.Request) {
|
|||||||
if ok := user.CanProceed(rq, "update-header-links"); !ok {
|
if ok := user.CanProceed(rq, "update-header-links"); !ok {
|
||||||
var lc = l18n.FromRequest(rq)
|
var lc = l18n.FromRequest(rq)
|
||||||
viewutil.HttpErr(viewutil.MetaFrom(w, rq), http.StatusForbidden, cfg.HomeHypha, lc.Get("ui.header_no_rights"))
|
viewutil.HttpErr(viewutil.MetaFrom(w, rq), http.StatusForbidden, cfg.HomeHypha, lc.Get("ui.header_no_rights"))
|
||||||
slog.Info("No rights to update header links")
|
log.Println("Rejected", rq.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
slog.Info("Updated header links")
|
|
||||||
shroom.SetHeaderLinks()
|
shroom.SetHeaderLinks()
|
||||||
http.Redirect(w, rq, "/", http.StatusSeeOther)
|
http.Redirect(w, rq, "/", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
@ -127,14 +122,13 @@ func handlerAbout(w http.ResponseWriter, rq *http.Request) {
|
|||||||
lc = l18n.FromRequest(rq)
|
lc = l18n.FromRequest(rq)
|
||||||
title = lc.Get("ui.about_title", &l18n.Replacements{"name": cfg.WikiName})
|
title = lc.Get("ui.about_title", &l18n.Replacements{"name": cfg.WikiName})
|
||||||
)
|
)
|
||||||
_, err := io.WriteString(w, viewutil.Base(
|
_, err := io.WriteString(w, views.Base(
|
||||||
viewutil.MetaFrom(w, rq),
|
viewutil.MetaFrom(w, rq),
|
||||||
title,
|
title,
|
||||||
AboutHTML(lc),
|
views.AboutHTML(lc),
|
||||||
map[string]string{},
|
|
||||||
))
|
))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to write About template", "err", err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,7 +143,7 @@ func handlerStyle(w http.ResponseWriter, rq *http.Request) {
|
|||||||
}
|
}
|
||||||
_, err = io.Copy(w, file)
|
_, err = io.Copy(w, file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to write stylesheet; proceeding anyway", "err", err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
}
|
}
|
||||||
@ -164,7 +158,7 @@ func handlerRobotsTxt(w http.ResponseWriter, rq *http.Request) {
|
|||||||
}
|
}
|
||||||
_, err = io.Copy(w, file)
|
_, err = io.Copy(w, file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to write robots.txt; proceeding anyway", "err", err)
|
log.Println()
|
||||||
}
|
}
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
}
|
}
|
||||||
@ -174,13 +168,11 @@ func handlerTitleSearch(w http.ResponseWriter, rq *http.Request) {
|
|||||||
_ = rq.ParseForm()
|
_ = rq.ParseForm()
|
||||||
var (
|
var (
|
||||||
query = rq.FormValue("q")
|
query = rq.FormValue("q")
|
||||||
hyphaName = util.CanonicalName(query)
|
|
||||||
_, nameFree = hyphae.AreFreeNames(hyphaName)
|
|
||||||
results []string
|
results []string
|
||||||
)
|
)
|
||||||
for hyphaName := range shroom.YieldHyphaNamesContainingString(query) {
|
for hyphaName := range shroom.YieldHyphaNamesContainingString(query) {
|
||||||
results = append(results, hyphaName)
|
results = append(results, hyphaName)
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
viewTitleSearch(viewutil.MetaFrom(w, rq), query, hyphaName, !nameFree, results)
|
viewTitleSearch(viewutil.MetaFrom(w, rq), query, results)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,10 +3,8 @@
|
|||||||
{{define "body"}}
|
{{define "body"}}
|
||||||
<main class="main-width">
|
<main class="main-width">
|
||||||
<h1>{{block "search results for" .Query}}Search results for ‘{{.}}’{{end}}</h1>
|
<h1>{{block "search results for" .Query}}Search results for ‘{{.}}’{{end}}</h1>
|
||||||
{{if .MatchedHyphaName}}
|
|
||||||
<p>{{block "go to hypha" .}}Go to hypha <a class="wikilink{{if .HasExactMatch | not}} wikilink_new{{end}}" href="/hypha/{{.MatchedHyphaName}}">{{beautifulName .MatchedHyphaName}}</a>.{{end}}</p>
|
|
||||||
{{end}}
|
|
||||||
{{if len .Results}}
|
{{if len .Results}}
|
||||||
|
<p>{{block "search desc" .}}Every hypha name has been compared with the query. Hyphae that have matched the query are listed below.{{end}}</p>
|
||||||
<ol>
|
<ol>
|
||||||
{{range .Results}}
|
{{range .Results}}
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@ -2,9 +2,8 @@ package misc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
|
"github.com/bouncepaw/mycorrhiza/viewutil"
|
||||||
"github.com/bouncepaw/mycorrhiza/web/viewutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -13,11 +12,11 @@ var (
|
|||||||
chainList, chainTitleSearch viewutil.Chain
|
chainList, chainTitleSearch viewutil.Chain
|
||||||
ruTranslation = `
|
ruTranslation = `
|
||||||
{{define "list of hyphae"}}Список гиф{{end}}
|
{{define "list of hyphae"}}Список гиф{{end}}
|
||||||
{{define "search:"}}Поиск: {{.}}{{end}}
|
{{define "search:"}}Поиск:{{end}}
|
||||||
{{define "search results for"}}Результаты поиска для «{{.}}»{{end}}
|
{{define "search results for"}}Результаты поиска для «{{.}}»{{end}}
|
||||||
{{define "search no results"}}Ничего не найдено.{{end}}
|
{{define "search desc"}}Название каждой из существующих гиф сопоставлено с запросом. Подходящие гифы приведены ниже.{{end}}
|
||||||
|
{{define "search no results"}}Ничего не найдено{{end}}
|
||||||
{{define "x total"}}{{.}} всего.{{end}}
|
{{define "x total"}}{{.}} всего.{{end}}
|
||||||
{{define "go to hypha"}}Перейти к гифе <a class="wikilink{{if .HasExactMatch | not}} wikilink_new{{end}}" href="/hypha/{{.MatchedHyphaName}}">{{beautifulName .MatchedHyphaName}}</a>.{{end}}
|
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -49,16 +48,12 @@ type titleSearchData struct {
|
|||||||
*viewutil.BaseData
|
*viewutil.BaseData
|
||||||
Query string
|
Query string
|
||||||
Results []string
|
Results []string
|
||||||
MatchedHyphaName string
|
|
||||||
HasExactMatch bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func viewTitleSearch(meta viewutil.Meta, query string, hyphaName string, hasExactMatch bool, results []string) {
|
func viewTitleSearch(meta viewutil.Meta, query string, results []string) {
|
||||||
viewutil.ExecutePage(meta, chainTitleSearch, titleSearchData{
|
viewutil.ExecutePage(meta, chainTitleSearch, titleSearchData{
|
||||||
BaseData: &viewutil.BaseData{},
|
BaseData: &viewutil.BaseData{},
|
||||||
Query: query,
|
Query: query,
|
||||||
Results: results,
|
Results: results,
|
||||||
MatchedHyphaName: hyphaName,
|
|
||||||
HasExactMatch: hasExactMatch,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,17 +2,11 @@ package mycoopts
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"github.com/bouncepaw/mycomarkup/v5/options"
|
||||||
"html"
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
"path/filepath"
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/cfg"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/interwiki"
|
"github.com/bouncepaw/mycorrhiza/interwiki"
|
||||||
"github.com/bouncepaw/mycorrhiza/l18n"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/util"
|
"github.com/bouncepaw/mycorrhiza/util"
|
||||||
|
|
||||||
"git.sr.ht/~bouncepaw/mycomarkup/v5/options"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func MarkupOptions(hyphaName string) options.Options {
|
func MarkupOptions(hyphaName string) options.Options {
|
||||||
@ -58,58 +52,3 @@ func MarkupOptions(hyphaName string) options.Options {
|
|||||||
ImgSrcFormatForInterwikiPrefix: interwiki.ImgSrcFormatFor,
|
ImgSrcFormatForInterwikiPrefix: interwiki.ImgSrcFormatFor,
|
||||||
}.FillTheRest()
|
}.FillTheRest()
|
||||||
}
|
}
|
||||||
|
|
||||||
func mediaRaw(h *hyphae.MediaHypha) string {
|
|
||||||
return Media(h, l18n.New("en", "en"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Media(h *hyphae.MediaHypha, lc *l18n.Localizer) string {
|
|
||||||
name := html.EscapeString(h.CanonicalName())
|
|
||||||
|
|
||||||
switch filepath.Ext(h.MediaFilePath()) {
|
|
||||||
case ".jpg", ".gif", ".png", ".webp", ".svg", ".ico":
|
|
||||||
return fmt.Sprintf(
|
|
||||||
`<div class="binary-container binary-container_with-img">
|
|
||||||
<a href="/binary/%s"><img src="/binary/%s"/></a>
|
|
||||||
</div>`,
|
|
||||||
name, name,
|
|
||||||
)
|
|
||||||
|
|
||||||
case ".ogg", ".webm", ".mp4":
|
|
||||||
return fmt.Sprintf(
|
|
||||||
`<div class="binary-container binary-container_with-video">
|
|
||||||
<video controls>
|
|
||||||
<source src="/binary/%s"/>
|
|
||||||
<p>%s <a href="/binary/%s">%s</a></p>
|
|
||||||
</video>
|
|
||||||
</div>`,
|
|
||||||
name,
|
|
||||||
html.EscapeString(lc.Get("ui.media_novideo")),
|
|
||||||
name,
|
|
||||||
html.EscapeString(lc.Get("ui.media_novideo_link")),
|
|
||||||
)
|
|
||||||
|
|
||||||
case ".mp3", ".wav", ".flac":
|
|
||||||
return fmt.Sprintf(
|
|
||||||
`<div class="binary-container binary-container_with-audio">
|
|
||||||
<audio controls>
|
|
||||||
<source src="/binary/%s"/>
|
|
||||||
<p>%s <a href="/binary/%s">%s</a></p>
|
|
||||||
</audio>
|
|
||||||
</div>`,
|
|
||||||
name,
|
|
||||||
html.EscapeString(lc.Get("ui.media_noaudio")),
|
|
||||||
name,
|
|
||||||
html.EscapeString(lc.Get("ui.media_noaudio_link")),
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf(
|
|
||||||
`<div class="binary-container binary-container_with-nothing">
|
|
||||||
<p><a href="/binary/%s">%s</a></p>
|
|
||||||
</div>`,
|
|
||||||
name,
|
|
||||||
html.EscapeString(lc.Get("ui.media_download")),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
37
mycoopts/view.qtpl
Normal file
37
mycoopts/view.qtpl
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{% import "path/filepath" %}
|
||||||
|
|
||||||
|
{% import "github.com/bouncepaw/mycorrhiza/hyphae" %}
|
||||||
|
{% import "github.com/bouncepaw/mycorrhiza/l18n" %}
|
||||||
|
|
||||||
|
{% func mediaRaw(h *hyphae.MediaHypha) %}{%= Media(h, l18n.New("en", "en")) %}{% endfunc %}
|
||||||
|
|
||||||
|
{% func Media(h *hyphae.MediaHypha, lc *l18n.Localizer) %}
|
||||||
|
{% switch filepath.Ext(h.MediaFilePath()) %}
|
||||||
|
|
||||||
|
{% case ".jpg", ".gif", ".png", ".webp", ".svg", ".ico" %}
|
||||||
|
<div class="binary-container binary-container_with-img">
|
||||||
|
<a href="/binary/{%s= h.CanonicalName() %}"><img src="/binary/{%s= h.CanonicalName() %}"/></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% case ".ogg", ".webm", ".mp4" %}
|
||||||
|
<div class="binary-container binary-container_with-video">
|
||||||
|
<video controls>
|
||||||
|
<source src="/binary/{%s= h.CanonicalName() %}"/>
|
||||||
|
<p>{%s lc.Get("ui.media_novideo") %} <a href="/binary/{%s= h.CanonicalName() %}">{%s lc.Get("ui.media_novideo_link") %}</a></p>
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% case ".mp3" %}
|
||||||
|
<div class="binary-container binary-container_with-audio">
|
||||||
|
<audio controls>
|
||||||
|
<source src="/binary/{%s= h.CanonicalName() %}"/>
|
||||||
|
<p>{%s lc.Get("ui.media_noaudio") %} <a href="/binary/{%s= h.CanonicalName() %}">{%s lc.Get("ui.media_noaudio_link") %}</a></p>
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% default %}
|
||||||
|
<div class="binary-container binary-container_with-nothing">
|
||||||
|
<p><a href="/binary/{%s= h.CanonicalName() %}">{%s lc.Get("ui.media_download") %}</a></p>
|
||||||
|
</div>
|
||||||
|
{% endswitch %}
|
||||||
|
{% endfunc %}
|
||||||
190
mycoopts/view.qtpl.go
Normal file
190
mycoopts/view.qtpl.go
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
// Code generated by qtc from "view.qtpl". DO NOT EDIT.
|
||||||
|
// See https://github.com/valyala/quicktemplate for details.
|
||||||
|
|
||||||
|
//line mycoopts/view.qtpl:1
|
||||||
|
package mycoopts
|
||||||
|
|
||||||
|
//line mycoopts/view.qtpl:1
|
||||||
|
import "path/filepath"
|
||||||
|
|
||||||
|
//line mycoopts/view.qtpl:3
|
||||||
|
import "github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
|
|
||||||
|
//line mycoopts/view.qtpl:4
|
||||||
|
import "github.com/bouncepaw/mycorrhiza/l18n"
|
||||||
|
|
||||||
|
//line mycoopts/view.qtpl:6
|
||||||
|
import (
|
||||||
|
qtio422016 "io"
|
||||||
|
|
||||||
|
qt422016 "github.com/valyala/quicktemplate"
|
||||||
|
)
|
||||||
|
|
||||||
|
//line mycoopts/view.qtpl:6
|
||||||
|
var (
|
||||||
|
_ = qtio422016.Copy
|
||||||
|
_ = qt422016.AcquireByteBuffer
|
||||||
|
)
|
||||||
|
|
||||||
|
//line mycoopts/view.qtpl:6
|
||||||
|
func streammediaRaw(qw422016 *qt422016.Writer, h *hyphae.MediaHypha) {
|
||||||
|
//line mycoopts/view.qtpl:6
|
||||||
|
StreamMedia(qw422016, h, l18n.New("en", "en"))
|
||||||
|
//line mycoopts/view.qtpl:6
|
||||||
|
}
|
||||||
|
|
||||||
|
//line mycoopts/view.qtpl:6
|
||||||
|
func writemediaRaw(qq422016 qtio422016.Writer, h *hyphae.MediaHypha) {
|
||||||
|
//line mycoopts/view.qtpl:6
|
||||||
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
|
//line mycoopts/view.qtpl:6
|
||||||
|
streammediaRaw(qw422016, h)
|
||||||
|
//line mycoopts/view.qtpl:6
|
||||||
|
qt422016.ReleaseWriter(qw422016)
|
||||||
|
//line mycoopts/view.qtpl:6
|
||||||
|
}
|
||||||
|
|
||||||
|
//line mycoopts/view.qtpl:6
|
||||||
|
func mediaRaw(h *hyphae.MediaHypha) string {
|
||||||
|
//line mycoopts/view.qtpl:6
|
||||||
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
|
//line mycoopts/view.qtpl:6
|
||||||
|
writemediaRaw(qb422016, h)
|
||||||
|
//line mycoopts/view.qtpl:6
|
||||||
|
qs422016 := string(qb422016.B)
|
||||||
|
//line mycoopts/view.qtpl:6
|
||||||
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
|
//line mycoopts/view.qtpl:6
|
||||||
|
return qs422016
|
||||||
|
//line mycoopts/view.qtpl:6
|
||||||
|
}
|
||||||
|
|
||||||
|
//line mycoopts/view.qtpl:8
|
||||||
|
func StreamMedia(qw422016 *qt422016.Writer, h *hyphae.MediaHypha, lc *l18n.Localizer) {
|
||||||
|
//line mycoopts/view.qtpl:8
|
||||||
|
qw422016.N().S(`
|
||||||
|
`)
|
||||||
|
//line mycoopts/view.qtpl:9
|
||||||
|
switch filepath.Ext(h.MediaFilePath()) {
|
||||||
|
//line mycoopts/view.qtpl:11
|
||||||
|
case ".jpg", ".gif", ".png", ".webp", ".svg", ".ico":
|
||||||
|
//line mycoopts/view.qtpl:11
|
||||||
|
qw422016.N().S(`
|
||||||
|
<div class="binary-container binary-container_with-img">
|
||||||
|
<a href="/binary/`)
|
||||||
|
//line mycoopts/view.qtpl:13
|
||||||
|
qw422016.N().S(h.CanonicalName())
|
||||||
|
//line mycoopts/view.qtpl:13
|
||||||
|
qw422016.N().S(`"><img src="/binary/`)
|
||||||
|
//line mycoopts/view.qtpl:13
|
||||||
|
qw422016.N().S(h.CanonicalName())
|
||||||
|
//line mycoopts/view.qtpl:13
|
||||||
|
qw422016.N().S(`"/></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
`)
|
||||||
|
//line mycoopts/view.qtpl:16
|
||||||
|
case ".ogg", ".webm", ".mp4":
|
||||||
|
//line mycoopts/view.qtpl:16
|
||||||
|
qw422016.N().S(`
|
||||||
|
<div class="binary-container binary-container_with-video">
|
||||||
|
<video controls>
|
||||||
|
<source src="/binary/`)
|
||||||
|
//line mycoopts/view.qtpl:19
|
||||||
|
qw422016.N().S(h.CanonicalName())
|
||||||
|
//line mycoopts/view.qtpl:19
|
||||||
|
qw422016.N().S(`"/>
|
||||||
|
<p>`)
|
||||||
|
//line mycoopts/view.qtpl:20
|
||||||
|
qw422016.E().S(lc.Get("ui.media_novideo"))
|
||||||
|
//line mycoopts/view.qtpl:20
|
||||||
|
qw422016.N().S(` <a href="/binary/`)
|
||||||
|
//line mycoopts/view.qtpl:20
|
||||||
|
qw422016.N().S(h.CanonicalName())
|
||||||
|
//line mycoopts/view.qtpl:20
|
||||||
|
qw422016.N().S(`">`)
|
||||||
|
//line mycoopts/view.qtpl:20
|
||||||
|
qw422016.E().S(lc.Get("ui.media_novideo_link"))
|
||||||
|
//line mycoopts/view.qtpl:20
|
||||||
|
qw422016.N().S(`</a></p>
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
`)
|
||||||
|
//line mycoopts/view.qtpl:24
|
||||||
|
case ".mp3":
|
||||||
|
//line mycoopts/view.qtpl:24
|
||||||
|
qw422016.N().S(`
|
||||||
|
<div class="binary-container binary-container_with-audio">
|
||||||
|
<audio controls>
|
||||||
|
<source src="/binary/`)
|
||||||
|
//line mycoopts/view.qtpl:27
|
||||||
|
qw422016.N().S(h.CanonicalName())
|
||||||
|
//line mycoopts/view.qtpl:27
|
||||||
|
qw422016.N().S(`"/>
|
||||||
|
<p>`)
|
||||||
|
//line mycoopts/view.qtpl:28
|
||||||
|
qw422016.E().S(lc.Get("ui.media_noaudio"))
|
||||||
|
//line mycoopts/view.qtpl:28
|
||||||
|
qw422016.N().S(` <a href="/binary/`)
|
||||||
|
//line mycoopts/view.qtpl:28
|
||||||
|
qw422016.N().S(h.CanonicalName())
|
||||||
|
//line mycoopts/view.qtpl:28
|
||||||
|
qw422016.N().S(`">`)
|
||||||
|
//line mycoopts/view.qtpl:28
|
||||||
|
qw422016.E().S(lc.Get("ui.media_noaudio_link"))
|
||||||
|
//line mycoopts/view.qtpl:28
|
||||||
|
qw422016.N().S(`</a></p>
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
`)
|
||||||
|
//line mycoopts/view.qtpl:32
|
||||||
|
default:
|
||||||
|
//line mycoopts/view.qtpl:32
|
||||||
|
qw422016.N().S(`
|
||||||
|
<div class="binary-container binary-container_with-nothing">
|
||||||
|
<p><a href="/binary/`)
|
||||||
|
//line mycoopts/view.qtpl:34
|
||||||
|
qw422016.N().S(h.CanonicalName())
|
||||||
|
//line mycoopts/view.qtpl:34
|
||||||
|
qw422016.N().S(`">`)
|
||||||
|
//line mycoopts/view.qtpl:34
|
||||||
|
qw422016.E().S(lc.Get("ui.media_download"))
|
||||||
|
//line mycoopts/view.qtpl:34
|
||||||
|
qw422016.N().S(`</a></p>
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
//line mycoopts/view.qtpl:36
|
||||||
|
}
|
||||||
|
//line mycoopts/view.qtpl:36
|
||||||
|
qw422016.N().S(`
|
||||||
|
`)
|
||||||
|
//line mycoopts/view.qtpl:37
|
||||||
|
}
|
||||||
|
|
||||||
|
//line mycoopts/view.qtpl:37
|
||||||
|
func WriteMedia(qq422016 qtio422016.Writer, h *hyphae.MediaHypha, lc *l18n.Localizer) {
|
||||||
|
//line mycoopts/view.qtpl:37
|
||||||
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
|
//line mycoopts/view.qtpl:37
|
||||||
|
StreamMedia(qw422016, h, lc)
|
||||||
|
//line mycoopts/view.qtpl:37
|
||||||
|
qt422016.ReleaseWriter(qw422016)
|
||||||
|
//line mycoopts/view.qtpl:37
|
||||||
|
}
|
||||||
|
|
||||||
|
//line mycoopts/view.qtpl:37
|
||||||
|
func Media(h *hyphae.MediaHypha, lc *l18n.Localizer) string {
|
||||||
|
//line mycoopts/view.qtpl:37
|
||||||
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
|
//line mycoopts/view.qtpl:37
|
||||||
|
WriteMedia(qb422016, h, lc)
|
||||||
|
//line mycoopts/view.qtpl:37
|
||||||
|
qs422016 := string(qb422016.B)
|
||||||
|
//line mycoopts/view.qtpl:37
|
||||||
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
|
//line mycoopts/view.qtpl:37
|
||||||
|
return qs422016
|
||||||
|
//line mycoopts/view.qtpl:37
|
||||||
|
}
|
||||||
@ -3,9 +3,9 @@ package shroom
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/user"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/l18n"
|
"github.com/bouncepaw/mycorrhiza/l18n"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: get rid of this abomination
|
// TODO: get rid of this abomination
|
||||||
@ -2,12 +2,10 @@ package shroom
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/backlinks"
|
||||||
"github.com/bouncepaw/mycorrhiza/history"
|
"github.com/bouncepaw/mycorrhiza/history"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/backlinks"
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/categories"
|
"github.com/bouncepaw/mycorrhiza/user"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/user"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Delete deletes the hypha and makes a history record about that.
|
// Delete deletes the hypha and makes a history record about that.
|
||||||
@ -32,7 +30,6 @@ func Delete(u *user.User, h hyphae.ExistingHypha) error {
|
|||||||
return hop.Errs[0]
|
return hop.Errs[0]
|
||||||
}
|
}
|
||||||
backlinks.UpdateBacklinksAfterDelete(h, originalText)
|
backlinks.UpdateBacklinksAfterDelete(h, originalText)
|
||||||
categories.RemoveHyphaFromAllCategories(h.CanonicalName())
|
|
||||||
hyphae.DeleteHypha(h)
|
hyphae.DeleteHypha(h)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -1,16 +1,14 @@
|
|||||||
package shroom
|
package shroom
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"github.com/bouncepaw/mycomarkup/v5"
|
||||||
|
"github.com/bouncepaw/mycomarkup/v5/blocks"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/cfg"
|
"github.com/bouncepaw/mycomarkup/v5/mycocontext"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
"github.com/bouncepaw/mycorrhiza/mycoopts"
|
"github.com/bouncepaw/mycorrhiza/mycoopts"
|
||||||
"github.com/bouncepaw/mycorrhiza/web/viewutil"
|
"github.com/bouncepaw/mycorrhiza/viewutil"
|
||||||
|
"os"
|
||||||
"git.sr.ht/~bouncepaw/mycomarkup/v5"
|
|
||||||
"git.sr.ht/~bouncepaw/mycomarkup/v5/blocks"
|
|
||||||
"git.sr.ht/~bouncepaw/mycomarkup/v5/mycocontext"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetHeaderLinks initializes header links by reading the configured hypha, if there is any, or resorting to default values.
|
// SetHeaderLinks initializes header links by reading the configured hypha, if there is any, or resorting to default values.
|
||||||
21
shroom/log.go
Normal file
21
shroom/log.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package shroom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func rejectRenameLog(h hyphae.Hypha, u *user.User, errmsg string) {
|
||||||
|
log.Printf("Reject rename ‘%s’ by @%s: %s\n", h.CanonicalName(), u.Name, errmsg)
|
||||||
|
}
|
||||||
|
func rejectRemoveMediaLog(h hyphae.Hypha, u *user.User, errmsg string) {
|
||||||
|
log.Printf("Reject remove media ‘%s’ by @%s: %s\n", h.CanonicalName(), u.Name, errmsg)
|
||||||
|
}
|
||||||
|
func rejectEditLog(h hyphae.Hypha, u *user.User, errmsg string) {
|
||||||
|
log.Printf("Reject edit ‘%s’ by @%s: %s\n", h.CanonicalName(), u.Name, errmsg)
|
||||||
|
}
|
||||||
|
func rejectUploadMediaLog(h hyphae.Hypha, u *user.User, errmsg string) {
|
||||||
|
log.Printf("Reject upload media ‘%s’ by @%s: %s\n", h.CanonicalName(), u.Name, errmsg)
|
||||||
|
}
|
||||||
@ -3,18 +3,16 @@ package shroom
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"github.com/bouncepaw/mycorrhiza/backlinks"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/categories"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/history"
|
"github.com/bouncepaw/mycorrhiza/history"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/backlinks"
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/categories"
|
"github.com/bouncepaw/mycorrhiza/user"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/cfg"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/files"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/user"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/util"
|
"github.com/bouncepaw/mycorrhiza/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -43,10 +41,8 @@ func Rename(oldHypha hyphae.ExistingHypha, newName string, recursive bool, leave
|
|||||||
var (
|
var (
|
||||||
re = regexp.MustCompile(`(?i)` + oldHypha.CanonicalName())
|
re = regexp.MustCompile(`(?i)` + oldHypha.CanonicalName())
|
||||||
replaceName = func(str string) string {
|
replaceName = func(str string) string {
|
||||||
namepart := strings.TrimPrefix(str, files.HyphaeDir())
|
// Can we drop that util.CanonicalName?
|
||||||
// Can we drop that util.CanonicalName?:
|
return re.ReplaceAllString(util.CanonicalName(str), newName)
|
||||||
replaced := re.ReplaceAllString(util.CanonicalName(namepart), newName)
|
|
||||||
return path.Join(files.HyphaeDir(), replaced)
|
|
||||||
}
|
}
|
||||||
hyphaeToRename = findHyphaeToRename(oldHypha, recursive)
|
hyphaeToRename = findHyphaeToRename(oldHypha, recursive)
|
||||||
renameMap, err = renamingPairs(hyphaeToRename, replaceName)
|
renameMap, err = renamingPairs(hyphaeToRename, replaceName)
|
||||||
@ -79,7 +75,7 @@ func Rename(oldHypha hyphae.ExistingHypha, newName string, recursive bool, leave
|
|||||||
for _, h := range hyphaeToRename {
|
for _, h := range hyphaeToRename {
|
||||||
var (
|
var (
|
||||||
oldName = h.CanonicalName()
|
oldName = h.CanonicalName()
|
||||||
newName = re.ReplaceAllString(oldName, newName)
|
newName = replaceName(oldName)
|
||||||
)
|
)
|
||||||
hyphae.RenameHyphaTo(h, newName, replaceName)
|
hyphae.RenameHyphaTo(h, newName, replaceName)
|
||||||
backlinks.UpdateBacklinksAfterRename(h, oldName)
|
backlinks.UpdateBacklinksAfterRename(h, oldName)
|
||||||
@ -97,13 +93,9 @@ func Rename(oldHypha hyphae.ExistingHypha, newName string, recursive bool, leave
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirectionTemplate = `=> %[1]s | 👁️➡️ %[2]s
|
|
||||||
<= %[1]s | full
|
|
||||||
`
|
|
||||||
|
|
||||||
func leaveRedirection(oldName, newName string, hop *history.Op) error {
|
func leaveRedirection(oldName, newName string, hop *history.Op) error {
|
||||||
var (
|
var (
|
||||||
text = fmt.Sprintf(redirectionTemplate, newName, util.BeautifulName(newName))
|
text = fmt.Sprintf("=> %s | ✏️ %s\n", newName, util.BeautifulName(newName))
|
||||||
emptyHypha = hyphae.ByName(oldName)
|
emptyHypha = hyphae.ByName(oldName)
|
||||||
)
|
)
|
||||||
switch emptyHypha := emptyHypha.(type) {
|
switch emptyHypha := emptyHypha.(type) {
|
||||||
@ -3,7 +3,7 @@ package shroom
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
"github.com/bouncepaw/mycorrhiza/util"
|
"github.com/bouncepaw/mycorrhiza/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -4,8 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/history"
|
"github.com/bouncepaw/mycorrhiza/history"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/user"
|
"github.com/bouncepaw/mycorrhiza/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RemoveMedia removes media from the media hypha and makes a history record about that. If it only had media, the hypha will be deleted. If it also had text, the hypha will become textual.
|
// RemoveMedia removes media from the media hypha and makes a history record about that. If it only had media, the hypha will be deleted. If it also had text, the hypha will become textual.
|
||||||
@ -4,19 +4,17 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/backlinks"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/files"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/history"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/mimetype"
|
||||||
|
"github.com/bouncepaw/mycorrhiza/user"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/history"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/backlinks"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/files"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/hyphae"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/mimetype"
|
|
||||||
"github.com/bouncepaw/mycorrhiza/internal/user"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func historyMessageForTextUpload(h hyphae.Hypha, userMessage string) string {
|
func historyMessageForTextUpload(h hyphae.Hypha, userMessage string) string {
|
||||||
@ -80,9 +78,7 @@ func UploadText(h hyphae.Hypha, data []byte, userMessage string, u *user.User) e
|
|||||||
|
|
||||||
switch h := h.(type) {
|
switch h := h.(type) {
|
||||||
case *hyphae.EmptyHypha:
|
case *hyphae.EmptyHypha:
|
||||||
parts := []string{files.HyphaeDir()}
|
H := hyphae.ExtendEmptyToTextual(h, filepath.Join(files.HyphaeDir(), h.CanonicalName()+".myco"))
|
||||||
parts = append(parts, strings.Split(h.CanonicalName()+".myco", "\\")...)
|
|
||||||
H := hyphae.ExtendEmptyToTextual(h, filepath.Join(parts...))
|
|
||||||
|
|
||||||
err := writeTextToDisk(H, data, hop)
|
err := writeTextToDisk(H, data, hop)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -143,8 +139,7 @@ func writeMediaToDisk(h hyphae.Hypha, mime string, data []byte) (string, error)
|
|||||||
var (
|
var (
|
||||||
ext = mimetype.ToExtension(mime)
|
ext = mimetype.ToExtension(mime)
|
||||||
// That's where the file will go
|
// That's where the file will go
|
||||||
|
uploadedFilePath = filepath.Join(files.HyphaeDir(), h.CanonicalName()+ext)
|
||||||
uploadedFilePath = filepath.Join(append([]string{files.HyphaeDir()}, strings.Split(h.CanonicalName()+ext, "\\")...)...)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(uploadedFilePath), 0777); err != nil {
|
if err := os.MkdirAll(filepath.Dir(uploadedFilePath), 0777); err != nil {
|
||||||
@ -201,7 +196,7 @@ func UploadBinary(h hyphae.Hypha, mime string, file multipart.File, u *user.User
|
|||||||
if err := history.Rename(prevFilePath, uploadedFilePath); err != nil {
|
if err := history.Rename(prevFilePath, uploadedFilePath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
slog.Info("Move file", "from", prevFilePath, "to", uploadedFilePath)
|
log.Printf("Move ‘%s’ to ‘%s’\n", prevFilePath, uploadedFilePath)
|
||||||
h.SetMediaFilePath(uploadedFilePath)
|
h.SetMediaFilePath(uploadedFilePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user