mirror of
https://git.cyberia.club/cyberia/matrix-synapse-diskspace-janitor
synced 2024-05-09 12:06:34 +02:00
copypasting frontend from greenhouse + working on login
This commit is contained in:
parent
0699f8e01e
commit
847f0b0fb1
|
@ -1,4 +1,9 @@
|
|||
## matrix-synapse-state-groups-state-janitor
|
||||
## matrix-synapse-diskspace-janitor
|
||||
|
||||
![scruffy the janitor from futurama](frontend/static/images/scruffy.png)
|
||||
|
||||
_toilets and boilers, boilers and toilets_
|
||||
|
||||
|
||||
|
||||
Matrix-synapse (the matrix homeserver implementation) requires a postgres database server to operate.
|
||||
|
|
10
config.json
10
config.json
|
@ -1,6 +1,14 @@
|
|||
{
|
||||
"FrontendPort": 6712,
|
||||
"FrontendDomain": "matrix-diskspace-janitor.cyberia.club",
|
||||
"MatrixServerPublicDomain": "cyberia.club",
|
||||
"MatrixURL": "http://localhost:8080",
|
||||
"MatrixAdminToken": "changeme",
|
||||
"DatabaseType": "postgres",
|
||||
"DatabaseConnectionString": "host=localhost port=5432 user=synapse_user password=changeme database=synapse sslmode=disable"
|
||||
|
||||
"DatabaseConnectionString":
|
||||
"host=localhost port=5432 user=synapse_user password=changeme database=synapse sslmode=disable",
|
||||
|
||||
"MediaFolder": "/var/lib/matrix-synapse",
|
||||
"PostgresFolder": "/var/lib/postgresql"
|
||||
}
|
|
@ -206,7 +206,7 @@ func (model *DBModel) DeleteStateGroupsState(stateGroupIds []int64, startAt int)
|
|||
}
|
||||
|
||||
// https://dataedo.com/kb/query/postgresql/list-of-tables-by-their-size
|
||||
func (model *DBModel) GetDBTableSizes(roomId string) (tables []DBTableSize, err error) {
|
||||
func (model *DBModel) GetDBTableSizes() (tables []DBTableSize, err error) {
|
||||
|
||||
rows, err := model.DB.Query(
|
||||
`select schemaname as table_schema, relname as table_name, pg_relation_size(relid) as data_size
|
||||
|
|
302
frontend.go
302
frontend.go
|
@ -1,2 +1,304 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
errors "git.sequentialread.com/forest/pkg-errors"
|
||||
|
||||
"github.com/shengdoushi/base58"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
SessionId string
|
||||
UserID string
|
||||
ExpiresUnixMilli int64
|
||||
Flash *map[string]string
|
||||
}
|
||||
|
||||
type FrontendApp struct {
|
||||
Port int
|
||||
Domain string
|
||||
Router *http.ServeMux
|
||||
HTMLTemplates map[string]*template.Template
|
||||
cssHash string
|
||||
basicURLPathRegex *regexp.Regexp
|
||||
base58Regex *regexp.Regexp
|
||||
}
|
||||
|
||||
func initFrontend(config *Config) FrontendApp {
|
||||
|
||||
cssBytes, err := os.ReadFile(filepath.Join(".", "frontend", "static", "app.css"))
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "can't initFrontend because can't read cssBytes:"))
|
||||
}
|
||||
hashArray := sha256.Sum256(cssBytes)
|
||||
cssHash := base58.Encode(hashArray[:6], base58.BitcoinAlphabet)
|
||||
|
||||
app := FrontendApp{
|
||||
Port: config.FrontendPort,
|
||||
Domain: config.FrontendDomain,
|
||||
Router: http.NewServeMux(),
|
||||
HTMLTemplates: map[string]*template.Template{},
|
||||
basicURLPathRegex: regexp.MustCompile("(?i)[a-z0-9/?&_+-]+"),
|
||||
base58Regex: regexp.MustCompile("(?i)[a-z0-9_-]+"),
|
||||
cssHash: cssHash,
|
||||
}
|
||||
|
||||
// serve the homepage
|
||||
app.handleWithSession("/", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
|
||||
|
||||
userIsLoggedIn := session.UserID != ""
|
||||
if userIsLoggedIn {
|
||||
app.buildPageFromTemplate(responseWriter, request, session, "panel.html", nil)
|
||||
} else {
|
||||
if request.Method == "POST" {
|
||||
username := request.PostFormValue("username")
|
||||
password := request.PostFormValue("password")
|
||||
|
||||
success, err := matrixAdmin.Login(username, password)
|
||||
if err != nil {
|
||||
(*session.Flash)["error"] += "an error was thrown by the login process 😧"
|
||||
log.Println(errors.Wrap(err, "an error was thrown by the login process"))
|
||||
} else if success {
|
||||
session.UserID = username
|
||||
session.ExpiresUnixMilli = time.Now().Add(time.Hour * 24).UnixMilli()
|
||||
err = app.setSession(responseWriter, &session)
|
||||
if err != nil {
|
||||
log.Println(errors.Wrap(err, "setSession failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.buildPageFromTemplate(responseWriter, request, session, "login.html", nil)
|
||||
}
|
||||
})
|
||||
|
||||
// registerHowtoRoutes(&app)
|
||||
|
||||
// registerLoginRoutes(&app, emailService)
|
||||
|
||||
// registerProfileRoutes(&app)
|
||||
|
||||
// registerAdminPanelRoutes(&app)
|
||||
|
||||
app.reloadTemplates()
|
||||
|
||||
staticFilesDir := "./frontend/static"
|
||||
log.Printf("serving static files from %s", staticFilesDir)
|
||||
app.Router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(staticFilesDir))))
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func (app *FrontendApp) ListenAndServe() error {
|
||||
return http.ListenAndServe(fmt.Sprintf(":%d", app.Port), app.Router)
|
||||
}
|
||||
|
||||
func (app *FrontendApp) setCookie(responseWriter http.ResponseWriter, name, value string, lifetimeSeconds int, sameSite http.SameSite) {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#define_where_cookies_are_sent
|
||||
// The Domain attribute specifies which hosts are allowed to receive the cookie.
|
||||
// If unspecified, it defaults to the same host that set the cookie, excluding subdomains.
|
||||
// If Domain is specified, then subdomains are always included.
|
||||
// Therefore, specifying Domain is less restrictive than omitting it.
|
||||
// However, it can be helpful when subdomains need to share information about a user.
|
||||
|
||||
toSet := &http.Cookie{
|
||||
Name: name,
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
SameSite: sameSite,
|
||||
Path: "/",
|
||||
Value: value,
|
||||
MaxAge: lifetimeSeconds,
|
||||
}
|
||||
|
||||
http.SetCookie(responseWriter, toSet)
|
||||
}
|
||||
|
||||
func (app *FrontendApp) deleteCookie(responseWriter http.ResponseWriter, name string) {
|
||||
http.SetCookie(responseWriter, &http.Cookie{
|
||||
Name: name,
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Path: "/",
|
||||
Value: "",
|
||||
MaxAge: -1,
|
||||
})
|
||||
}
|
||||
|
||||
func (app *FrontendApp) getSession(request *http.Request, domain string) (Session, error) {
|
||||
toReturn := Session{
|
||||
Flash: &(map[string]string{}),
|
||||
}
|
||||
for _, cookie := range request.Cookies() {
|
||||
if cookie.Name == "sessionId" && app.base58Regex.MatchString(cookie.Value) {
|
||||
session, err := ReadJsonFile[Session](fmt.Sprintf("data/sessions/%s.json", cookie.Value))
|
||||
|
||||
if err == nil {
|
||||
if session.ExpiresUnixMilli > time.Now().UnixMilli() {
|
||||
toReturn.SessionId = cookie.Value
|
||||
toReturn.UserID = session.UserID
|
||||
}
|
||||
}
|
||||
//log.Printf("toReturn.SessionId %s\n", toReturn.SessionId)
|
||||
} else if cookie.Name == "flash" && cookie.Value != "" {
|
||||
bytes, err := base64.RawURLEncoding.DecodeString(cookie.Value)
|
||||
if err != nil {
|
||||
log.Printf("can't getSession because can't base64 decode flash cookie: %+v", err)
|
||||
return toReturn, err
|
||||
}
|
||||
flash := map[string]string{}
|
||||
err = json.Unmarshal(bytes, &flash)
|
||||
if err != nil {
|
||||
log.Printf("can't getSession because can't json parse the decoded flash cookie: %+v", err)
|
||||
return toReturn, err
|
||||
}
|
||||
toReturn.Flash = &flash
|
||||
}
|
||||
}
|
||||
return toReturn, nil
|
||||
}
|
||||
|
||||
func (app *FrontendApp) setSession(responseWriter http.ResponseWriter, session *Session) error {
|
||||
sessionIdBuffer := make([]byte, 32)
|
||||
rand.Read(sessionIdBuffer)
|
||||
sessionId := base58.Encode(sessionIdBuffer, base58.BitcoinAlphabet)
|
||||
|
||||
err := WriteJsonFile(fmt.Sprintf("data/sessions/%s.json", sessionId), *session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bytes, _ := json.MarshalIndent(session, "", " ")
|
||||
log.Printf("setSession(): %s %s\n", sessionId, string(bytes))
|
||||
|
||||
exipreInSeconds := int(time.Until(time.UnixMilli(session.ExpiresUnixMilli)).Seconds())
|
||||
app.setCookie(responseWriter, "sessionId", sessionId, exipreInSeconds, http.SameSiteStrictMode)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *FrontendApp) unhandledError(responseWriter http.ResponseWriter, request *http.Request, err error) {
|
||||
log.Printf("500 internal server error: %+v\n", err)
|
||||
|
||||
responseWriter.Header().Add("Content-Type", "text/plain")
|
||||
responseWriter.WriteHeader(http.StatusInternalServerError)
|
||||
responseWriter.Write([]byte("500 internal server error"))
|
||||
}
|
||||
|
||||
func (app *FrontendApp) handleWithSession(path string, handler func(http.ResponseWriter, *http.Request, Session)) {
|
||||
app.Router.HandleFunc(path, func(responseWriter http.ResponseWriter, request *http.Request) {
|
||||
session, err := app.getSession(request, app.Domain)
|
||||
|
||||
bytes, _ := json.MarshalIndent(session, "", " ")
|
||||
log.Printf("handleWithSession(): %s\n", string(bytes))
|
||||
|
||||
if err != nil {
|
||||
app.unhandledError(responseWriter, request, err)
|
||||
} else {
|
||||
handler(responseWriter, request, session)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (app *FrontendApp) buildPage(responseWriter http.ResponseWriter, request *http.Request, session Session, highlight, page template.HTML) {
|
||||
var buffer bytes.Buffer
|
||||
templateName := "page.html"
|
||||
pageTemplate, hasPageTemplate := app.HTMLTemplates[templateName]
|
||||
if !hasPageTemplate {
|
||||
panic(fmt.Errorf("template '%s' not found!", templateName))
|
||||
}
|
||||
err := pageTemplate.Execute(
|
||||
&buffer,
|
||||
struct {
|
||||
Session Session
|
||||
Highlight template.HTML
|
||||
Page template.HTML
|
||||
CSSHash string
|
||||
}{session, highlight, page, app.cssHash},
|
||||
)
|
||||
app.deleteCookie(responseWriter, "flash")
|
||||
|
||||
if err != nil {
|
||||
app.unhandledError(responseWriter, request, err)
|
||||
} else {
|
||||
io.Copy(responseWriter, &buffer)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *FrontendApp) renderTemplateToHTML(templateName string, data interface{}) (template.HTML, error) {
|
||||
var buffer bytes.Buffer
|
||||
desiredTemplate, hasTemplate := app.HTMLTemplates[templateName]
|
||||
if !hasTemplate {
|
||||
return "", fmt.Errorf("template '%s' not found!", templateName)
|
||||
}
|
||||
err := desiredTemplate.Execute(&buffer, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return template.HTML(buffer.String()), nil
|
||||
}
|
||||
|
||||
func (app *FrontendApp) buildPageFromTemplate(responseWriter http.ResponseWriter, request *http.Request, session Session, templateName string, data interface{}) {
|
||||
content, err := app.renderTemplateToHTML(templateName, data)
|
||||
if err != nil {
|
||||
app.unhandledError(responseWriter, request, err)
|
||||
} else {
|
||||
app.buildPage(responseWriter, request, session, template.HTML(""), content)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *FrontendApp) setFlash(responseWriter http.ResponseWriter, session Session, key, value string) {
|
||||
(*session.Flash)[key] += value
|
||||
bytes, err := json.Marshal((*session.Flash))
|
||||
if err != nil {
|
||||
log.Printf("can't setFlash because can't json marshal the flash map: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
app.setCookie(responseWriter, "flash", base64.RawURLEncoding.EncodeToString(bytes), 60, http.SameSiteStrictMode)
|
||||
}
|
||||
|
||||
func (app *FrontendApp) reloadTemplates() {
|
||||
|
||||
loadTemplate := func(filename string) *template.Template {
|
||||
newTemplateString, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
newTemplate, err := template.New(filename).Parse(string(newTemplateString))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return newTemplate
|
||||
}
|
||||
|
||||
frontendDirectory := "./frontend"
|
||||
//frontendVersion = hashTemplateAndStaticFiles(frontendDirectory)[:6]
|
||||
|
||||
fileInfos, err := os.ReadDir(frontendDirectory)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, fileInfo := range fileInfos {
|
||||
if !fileInfo.IsDir() && strings.Contains(fileInfo.Name(), ".gotemplate") {
|
||||
app.HTMLTemplates[strings.Replace(fileInfo.Name(), ".gotemplate", "", 1)] = loadTemplate(filepath.Join(frontendDirectory, fileInfo.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
11
frontend/login.gotemplate.html
Normal file
11
frontend/login.gotemplate.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
|
||||
|
||||
<div class="horizontal space-around">
|
||||
<form action="/" method="POST" class="box vertical">
|
||||
<h3>login</h3>
|
||||
<input type="text" name="username" placeholder="username"></input>
|
||||
<input type="password" name="password" placeholder="password"></input>
|
||||
|
||||
<input type="submit" value="Login"></input>
|
||||
</form>
|
||||
</div>
|
51
frontend/page.gotemplate.html
Normal file
51
frontend/page.gotemplate.html
Normal file
|
@ -0,0 +1,51 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>matrix-synapse diskspace janitor</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="shortcut icon" href="/static/favicon.png" />
|
||||
<link href="/static/app.css?v={{.CSSHash}}" rel="stylesheet">
|
||||
|
||||
<script src="/static/vendor/chart.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="header-container">
|
||||
<img src="static/images/scruffy.png"/>
|
||||
<h1>
|
||||
matrix-synapse diskspace janitor
|
||||
</h1>
|
||||
<div class="session-status float-right">
|
||||
{{if .Session.UserID }}
|
||||
{{ .Session.UserID }} | <a href="/logout">logout</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</header>
|
||||
<main>
|
||||
<section class="highlight">
|
||||
{{if index .Session.Flash "error"}}
|
||||
<pre class="flash error">{{index .Session.Flash "error"}}</pre>
|
||||
{{end}}
|
||||
{{if index .Session.Flash "info"}}
|
||||
<pre class="flash info">{{index .Session.Flash "info"}}</pre>
|
||||
{{end}}
|
||||
</section>
|
||||
<section class="page">
|
||||
{{.Page}}
|
||||
</section>
|
||||
</main>
|
||||
<footer>
|
||||
<div class="horizontal justify-center">
|
||||
<div class="cloud">
|
||||
a tool by <a href="https://sequentialread.com/">SequentialRead</a> |
|
||||
<a href="https://git.cyberia.club/cyberia/matrix-synapse-diskspace-janitor">Source Code</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
0
frontend/panel.gotemplate.html
Normal file
0
frontend/panel.gotemplate.html
Normal file
831
frontend/static/app.css
Normal file
831
frontend/static/app.css
Normal file
|
@ -0,0 +1,831 @@
|
|||
html {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system,system-ui,BlinkMacSystemFont,"Ubuntu",Roboto,"Segoe UI",sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
background-image: linear-gradient(to bottom, #0074ba 0%, #6c9ad1 100%);
|
||||
min-height: 100%;
|
||||
}
|
||||
header {
|
||||
background-image: linear-gradient(to right, #85bb2e 0%, #35bd1a88 100%);
|
||||
padding: 0.15rem 0.45rem;
|
||||
|
||||
box-shadow: 0 0 6rem 1rem #27026b8a;
|
||||
border-bottom-left-radius: 0.8rem;
|
||||
border-bottom-right-radius: 0.8rem;
|
||||
}
|
||||
header .header-container {
|
||||
padding: 0.25rem 0;
|
||||
border-bottom: 2px solid #c5d6d6aa;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.3rem;
|
||||
background: #5f7a7a;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
display: inline;
|
||||
letter-spacing: 0.2rem;
|
||||
padding: 0.1rem 0.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
header .shine-container {
|
||||
position: relative;
|
||||
}
|
||||
header .shine {
|
||||
opacity: 1;
|
||||
background-image: url(images/shine.png);
|
||||
background-size: cover;
|
||||
width: 1.8rem;
|
||||
height: 1.8rem;
|
||||
position: absolute;
|
||||
right:-1.1rem;
|
||||
top:-0.7rem;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, .price {
|
||||
font-family: 'Nunito',-apple-system,system-ui,BlinkMacSystemFont,"Ubuntu",Roboto,"Segoe UI",sans-serif;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 1em;
|
||||
padding-left: 2em;
|
||||
background-color: white;
|
||||
border-left: 5px solid #ccc;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
p, li {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
li {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
li::marker {
|
||||
font-weight: bold;
|
||||
}
|
||||
a {
|
||||
font-weight: 600;
|
||||
color: #66811a;
|
||||
}
|
||||
a:visited {
|
||||
font-weight: 600;
|
||||
color: #406e14;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
video {
|
||||
max-height: 90%;
|
||||
}
|
||||
footer {
|
||||
height: 100px;
|
||||
background: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #005b9c;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
footer .cloud {
|
||||
padding: 10px 20px;
|
||||
border-radius: 20px;
|
||||
background: #ffffff40;
|
||||
}
|
||||
|
||||
footer a {
|
||||
font-weight: normal;
|
||||
color: #101e58;
|
||||
}
|
||||
footer a:visited {
|
||||
color: #2f1058;
|
||||
}
|
||||
|
||||
header .session-status,
|
||||
header .session-status a {
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
header .session-status a {
|
||||
text-decoration-thickness: 2px;
|
||||
}
|
||||
header .session-status {
|
||||
margin-top: 0.2em;
|
||||
margin-right: 0.8em;
|
||||
}
|
||||
|
||||
|
||||
code {
|
||||
padding: 2px 4px;
|
||||
font-size: 90%;
|
||||
color: #862323;
|
||||
background-color: #965f5f1c;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
pre code,
|
||||
pre tt {
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
color: #222;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* red-purple inline code links */
|
||||
code a,
|
||||
a code {
|
||||
font-weight: 600;
|
||||
color: #af5214;
|
||||
text-emphasis-color: #af5214;
|
||||
text-decoration-thickness: 2px;
|
||||
}
|
||||
code a:hover,
|
||||
a:hover code,
|
||||
code a:visited,
|
||||
a:visited code {
|
||||
font-weight: 600;
|
||||
color: #973213;
|
||||
text-emphasis-color: #973213;
|
||||
text-decoration-thickness: 2px;
|
||||
}
|
||||
|
||||
main {
|
||||
/* background: #0000007e; */
|
||||
}
|
||||
.highlight {
|
||||
color: #eee4dd;
|
||||
padding: 2em 1em;
|
||||
}
|
||||
.highlight a {
|
||||
color: #b0e226;
|
||||
}
|
||||
|
||||
pre.flash {
|
||||
border: 2px solid gray;
|
||||
font-weight: bold;
|
||||
padding: 2em;
|
||||
border-radius: 8px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
pre.flash.error {
|
||||
border-color: #5e2416;
|
||||
background: #da9871;
|
||||
color: #5e2416;
|
||||
}
|
||||
pre.flash.info {
|
||||
border-color: #000000aa;
|
||||
background: #ffffff60;
|
||||
color: #12496e;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.admonition {
|
||||
border-left: 8px dashed gray;
|
||||
padding: 5px 20px;
|
||||
/* display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start; */
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.admonition.warning {
|
||||
border-left-color: #ffbb00;
|
||||
background: #e7d8ae;
|
||||
}
|
||||
.admonition.info {
|
||||
border-left-color: #289eff;
|
||||
background: #c7dffa;
|
||||
}
|
||||
.admonition.info code {
|
||||
background-color: #753d3d08;
|
||||
}
|
||||
.admonition.mascot {
|
||||
border-left-color: #00ffaa;
|
||||
background: #e8ffc3;
|
||||
}
|
||||
|
||||
.emoji-icon {
|
||||
margin-right: 20px;
|
||||
margin-top: 15px ;
|
||||
float: left;
|
||||
font-family: sans-serif;
|
||||
font-size: 60px;
|
||||
text-shadow: 2px 5px 20px #25202e40, 2px 5px 10px #25202e40, 2px 5px 3px #25202e40;
|
||||
}
|
||||
.emoji-icon img {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.install-command {
|
||||
background: #554;
|
||||
border-radius: 10px;
|
||||
box-shadow: inset 1px 1px 40px 0 #222;
|
||||
font-weight: bold;
|
||||
color:#e5f879;
|
||||
padding: 1em;
|
||||
white-space: pre-wrap;
|
||||
display: inline-block;
|
||||
margin: 10px 0;
|
||||
}
|
||||
@media screen and (max-width: 800px) {
|
||||
.install-command {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 640px) {
|
||||
.install-command {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
.install-command.small {
|
||||
font-size: 12px;
|
||||
}
|
||||
.white-pill {
|
||||
border: 1px solid gray;
|
||||
border-radius: 12px;
|
||||
background: white;
|
||||
box-shadow: 0.1rem 0.1rem 0.5rem 0 #00000060;
|
||||
padding: 2px 8px;
|
||||
color: #444;
|
||||
font-size: 16px;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.white-pill img {
|
||||
height: 20px;
|
||||
margin: -2px 5px -4px 5px;
|
||||
filter: brightness(1.4);
|
||||
}
|
||||
|
||||
|
||||
.page {
|
||||
padding: 2vw 4vw;
|
||||
border-top: 0.2rem dashed #438bc8;
|
||||
border-bottom: 0.2rem dashed #6999d0;
|
||||
/* border-bottom: 0.2rem dashed #8a7232; */
|
||||
background: #d9d9d9;
|
||||
color: #432;
|
||||
min-height: 20em;
|
||||
}
|
||||
.page.no-horizontal-margin {
|
||||
padding: 2vw 0;
|
||||
}
|
||||
@media screen and (max-width: 1024px) {
|
||||
.page {
|
||||
padding: 2vw 2vw;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 640px) {
|
||||
.page {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
.pagewidth {
|
||||
max-width: 1100px;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
float: right;
|
||||
}
|
||||
.image-large {
|
||||
margin: 1rem;
|
||||
width: 22rem;
|
||||
}
|
||||
.image-medium {
|
||||
margin: 1rem;
|
||||
width: 8rem;
|
||||
}
|
||||
.image-small {
|
||||
margin: 0.5rem;
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
.horizontal {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.margin-bottom {
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
.align-center {
|
||||
align-items: center;
|
||||
}
|
||||
.vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.money-unit {
|
||||
margin-right: 0.3em;
|
||||
}
|
||||
|
||||
input,button,textarea,
|
||||
input:hover,button:hover,textarea:hover,
|
||||
input:focus,button:focus,textarea:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
.js-form-submit-button {
|
||||
box-sizing: content-box;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.5rem;
|
||||
color: #333;
|
||||
font-size: 1em;
|
||||
}
|
||||
input:focus {
|
||||
border: 2px solid #029dfd;
|
||||
padding: calc(0.5rem - 1px);
|
||||
}
|
||||
.text-align-right {
|
||||
text-align: right;
|
||||
}
|
||||
input.short {
|
||||
width: 11em;
|
||||
}
|
||||
input.subdomain {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
/* Firefox */
|
||||
input[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
section.comments,
|
||||
.homepage-markdown pre,
|
||||
pre.telemetry {
|
||||
background-color: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 5px 5px 20px 0 #00000020;
|
||||
}
|
||||
|
||||
.homepage-markdown pre,
|
||||
pre.telemetry {
|
||||
padding: 20px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
pre.telemetry {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.homepage-mascot {
|
||||
width: 25vw;
|
||||
max-width: 180px;
|
||||
}
|
||||
@media screen and (max-width: 640px) {
|
||||
.homepage-mascot {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
section.comments h1,
|
||||
section.comments h2,
|
||||
section.comments h3 {
|
||||
padding-top: 20px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
section.comments a {
|
||||
font-weight: 600;
|
||||
color: #7333e0;
|
||||
}
|
||||
section.comments a:visited {
|
||||
font-weight: 600;
|
||||
color: #7e1cac;
|
||||
}
|
||||
|
||||
#sqr-comment-container {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
#sqr-comment-container .sqr-comment-bottom-row {
|
||||
padding-bottom: 0.4em;
|
||||
}
|
||||
|
||||
form.vertical input,
|
||||
form.horizontal.wrap input,
|
||||
form.vertical .js-form-submit-button {
|
||||
margin-top: 0.4rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.js-form-submit-button {
|
||||
padding: 0.25em 1em;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
}
|
||||
.form-footer {
|
||||
font-size: 0.85em;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
.fine-print {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
|
||||
.profile-form {
|
||||
width: 100%;
|
||||
background-color: #dfd5ca;
|
||||
border-radius: 1em;
|
||||
padding: 0.7em;
|
||||
}
|
||||
|
||||
.api-token,
|
||||
.external-domain {
|
||||
|
||||
border-bottom: 2px solid #44332255;
|
||||
margin: 0.5em;
|
||||
padding: 0.15em;
|
||||
}
|
||||
|
||||
.new-api-token {
|
||||
background-color: #619d00;
|
||||
color: white;
|
||||
border-radius: 0.5em;
|
||||
padding: 0.5em;
|
||||
border-bottom: 0;
|
||||
line-height: 2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.download-image {
|
||||
height: 32px;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
.os-image {
|
||||
height: 1.5em;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.invalid {
|
||||
color: red;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.horizontal.flip {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.space-around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.space-between {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.justify-left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.justify-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.flex-grow-2 {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: white;
|
||||
margin: 1rem;
|
||||
padding: 1rem 2rem;
|
||||
border-radius: 1rem;
|
||||
/* border: 0.2rem dashed #cba; */
|
||||
z-index: 1;
|
||||
}
|
||||
@media screen and (max-width: 640px) {
|
||||
.box {
|
||||
border-radius: 0;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.box img.expand {
|
||||
margin: 0 -6rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.tab-container {
|
||||
margin: 1rem 0.5rem;
|
||||
display: grid;
|
||||
}
|
||||
.tab-container input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-container > label:nth-of-type(1) {
|
||||
grid-column: 1 / 2;
|
||||
}
|
||||
.tab-container > label:nth-of-type(2) {
|
||||
grid-column: 2 / 4;
|
||||
}
|
||||
.tab-container > label:nth-of-type(3) {
|
||||
grid-column: 4 / 5;
|
||||
}
|
||||
|
||||
.tab-container > label {
|
||||
|
||||
margin: 0;
|
||||
grid-row: 1 / 2;
|
||||
border-top: 2px solid #00000000;
|
||||
padding: 1rem;
|
||||
border-bottom: 2px solid #c4c4c4;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
|
||||
color: #432;
|
||||
text-decoration: none;
|
||||
|
||||
font-family: 'Nunito',-apple-system,system-ui,BlinkMacSystemFont,"Ubuntu",Roboto,"Segoe UI",sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.tab-container input[type="radio"]:checked + label {
|
||||
border-top: 2px solid #8a7232;
|
||||
background: white;
|
||||
border-bottom: 2px solid #00000000;
|
||||
box-shadow: 0.1rem 0.5rem 1rem 0 #00000020;
|
||||
}
|
||||
.tab-container input[type="radio"]:checked + label + .tab-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tab-container.two-tabs .tab-content {
|
||||
grid-column: 1 / 4;
|
||||
}
|
||||
.tab-container.three-tabs .tab-content {
|
||||
grid-column: 1 / 5;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
grid-row: 2 / 3;
|
||||
|
||||
display: none;
|
||||
position: relative;
|
||||
|
||||
padding: 1rem;
|
||||
z-index: 1;
|
||||
background: white;
|
||||
box-shadow: 0.1rem 0.5rem 1rem 0 #00000020;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* this makes the tab-content for the selected tab visible */
|
||||
input[type="radio"].tab:checked+label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1150px) and (min-width: 901px) {
|
||||
.service-pricing .box {
|
||||
border-radius: 0;
|
||||
margin-left: -0.1rem;
|
||||
margin-right: -0.1rem;
|
||||
}
|
||||
.service-pricing .box:first-child {
|
||||
border-top-left-radius: 1rem;
|
||||
border-bottom-left-radius: 1rem;
|
||||
}
|
||||
.service-pricing .box:nth-child(2) {
|
||||
border-bottom-right-radius: 1rem;
|
||||
border-bottom-left-radius: 1rem;
|
||||
}
|
||||
.service-pricing .box:last-child {
|
||||
border-top-right-radius: 1rem;
|
||||
border-bottom-right-radius: 1rem;
|
||||
}
|
||||
.service-pricing .box.price-box {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.box.price-box {
|
||||
border-radius: 0.5rem;
|
||||
min-width: 11rem;
|
||||
padding: 1rem;
|
||||
padding-bottom: 0rem;
|
||||
margin-left: -1rem;
|
||||
margin-right: -1rem;
|
||||
box-shadow: 0 0.5rem 1rem 0 #00000070;
|
||||
border: 2px solid #ccc;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.price {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.big-price {
|
||||
font-size: 3.6rem;
|
||||
}
|
||||
|
||||
.med-price {
|
||||
font-size: 2.6rem;
|
||||
}
|
||||
|
||||
.price-box ul {
|
||||
margin-left: -1rem;
|
||||
align-self: baseline;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.admin-box {
|
||||
border-radius: 0.5rem;
|
||||
min-width: 11rem;
|
||||
padding: 1rem;
|
||||
padding-bottom: 0rem;
|
||||
box-shadow: 0 0.5rem 1rem 0 #00000070;
|
||||
border: 2px solid #ccc;
|
||||
}
|
||||
|
||||
.admin-box form {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.instance-group {
|
||||
min-height: 100px;
|
||||
width: 100%;
|
||||
border: 3px dashed rgb(173, 230, 109);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
.instance-group>h4,
|
||||
.instance-group>h3,
|
||||
.instance-group>h5 {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.instance {
|
||||
width: 100px;
|
||||
height: 120px;
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 0.3em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.instance.healthy {
|
||||
background-color: rgb(102, 241, 176);
|
||||
border: 1px solid rgb(56, 161, 165);
|
||||
}
|
||||
.instance.unhealthy {
|
||||
background-color: rgb(255, 116, 92);
|
||||
border: 1px solid rgb(165, 56, 74);
|
||||
}
|
||||
|
||||
.instance-name,
|
||||
.instance-ip {
|
||||
background-color: white;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.instance-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
flex-grow: 1;
|
||||
box-sizing: content-box;
|
||||
max-height: calc(100% - 40px);
|
||||
}
|
||||
.current-tenants, .pinned-tenants {
|
||||
display: inline-flex;
|
||||
writing-mode: vertical-lr;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
margin-right: 5px;
|
||||
padding: 2px;
|
||||
min-width: 10px;
|
||||
min-height: 100%;
|
||||
}
|
||||
.current-tenants {
|
||||
border: 1px solid rgb(51, 57, 116);
|
||||
}
|
||||
.pinned-tenants {
|
||||
border: 1px dashed gray;
|
||||
}
|
||||
.tenant {
|
||||
writing-mode: horizontal-tb;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
padding: 3px;
|
||||
font-size: 10px;
|
||||
}
|
||||
.thermometer {
|
||||
background-color: gray;
|
||||
min-height: 100%;
|
||||
width: 6px;
|
||||
padding: 2px;
|
||||
margin-right: 5px;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.thermometer.projected {
|
||||
margin-right: 0;
|
||||
}
|
||||
.marker {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
border: 2px solid red;
|
||||
color: #400;
|
||||
background-color: #fcc;
|
||||
border-radius: 4px;
|
||||
outline: 0;
|
||||
padding: 0.2em;
|
||||
}
|
BIN
frontend/static/favicon.png
Normal file
BIN
frontend/static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 324 B |
BIN
frontend/static/images/scruffy.png
Normal file
BIN
frontend/static/images/scruffy.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.8 KiB |
3
go.mod
3
go.mod
|
@ -1,4 +1,4 @@
|
|||
module git.cyberia.club/cyberia/matrix-synapse-state-groups-state-janitor
|
||||
module git.cyberia.club/cyberia/matrix-synapse-diskspace-janitor
|
||||
|
||||
go 1.19
|
||||
|
||||
|
@ -6,6 +6,7 @@ require (
|
|||
git.sequentialread.com/forest/config-lite v0.0.0-20220225195944-164dc71bce04 // indirect
|
||||
git.sequentialread.com/forest/pkg-errors v0.9.2 // indirect
|
||||
github.com/lib/pq v1.10.7 // indirect
|
||||
github.com/shengdoushi/base58 v1.0.0 // indirect
|
||||
github.com/texttheater/golang-levenshtein/levenshtein v0.0.0-20200805054039-cae8b0eaed6c // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
)
|
||||
|
|
2
go.sum
2
go.sum
|
@ -4,6 +4,8 @@ git.sequentialread.com/forest/pkg-errors v0.9.2 h1:j6pwbL6E+TmE7TD0tqRtGwuoCbCfO
|
|||
git.sequentialread.com/forest/pkg-errors v0.9.2/go.mod h1:8TkJ/f8xLWFIAid20aoqgDZcCj9QQt+FU+rk415XO1w=
|
||||
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
|
||||
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/shengdoushi/base58 v1.0.0 h1:tGe4o6TmdXFJWoI31VoSWvuaKxf0Px3gqa3sUWhAxBs=
|
||||
github.com/shengdoushi/base58 v1.0.0/go.mod h1:m5uIILfzcKMw6238iWAhP4l3s5+uXyF3+bJKUNhAL9I=
|
||||
github.com/texttheater/golang-levenshtein/levenshtein v0.0.0-20200805054039-cae8b0eaed6c h1:HelZ2kAFadG0La9d+4htN4HzQ68Bm2iM9qKMSMES6xg=
|
||||
github.com/texttheater/golang-levenshtein/levenshtein v0.0.0-20200805054039-cae8b0eaed6c/go.mod h1:JlzghshsemAMDGZLytTFY8C1JQxQPhnatWqNwUXjggo=
|
||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||
|
|
163
main.go
163
main.go
|
@ -1,23 +1,46 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
configlite "git.sequentialread.com/forest/config-lite"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
FrontendPort int
|
||||
FrontendDomain string
|
||||
MatrixURL string
|
||||
MatrixServerPublicDomain string
|
||||
AdminMatrixRoomId string
|
||||
MatrixAdminToken string
|
||||
DatabaseType string
|
||||
DatabaseConnectionString string
|
||||
MediaFolder string
|
||||
PostgresFolder string
|
||||
}
|
||||
|
||||
type JanitorState struct {
|
||||
LastScheduledTaskRunUnixMilli int64
|
||||
}
|
||||
|
||||
type DiskUsage struct {
|
||||
DiskSizeBytes int64
|
||||
OtherBytes int64
|
||||
MediaBytes int64
|
||||
PostgresBytes int64
|
||||
}
|
||||
|
||||
var isRunningScheduledTask bool
|
||||
var mutex sync.Mutex
|
||||
var matrixAdmin *MatrixAdmin
|
||||
|
||||
func main() {
|
||||
mutex = sync.Mutex{}
|
||||
|
||||
config := Config{}
|
||||
ignoreCommandlineFlags := []string{}
|
||||
err := configlite.ReadConfiguration("config.json", "JANITOR", ignoreCommandlineFlags, reflect.ValueOf(&config))
|
||||
|
@ -25,7 +48,78 @@ func main() {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
db := initDatabase(&config)
|
||||
validateConfig(&config)
|
||||
|
||||
//db := initDatabase(&config)
|
||||
matrixAdmin = initMatrixAdmin(&config)
|
||||
frontend := initFrontend(&config)
|
||||
|
||||
go frontend.ListenAndServe()
|
||||
|
||||
for {
|
||||
janitorState, err := ReadJsonFile[JanitorState]("data/janitorState.json")
|
||||
if err != nil {
|
||||
log.Printf("ERROR!: can't read data/janitorState.json: %+v\n", err)
|
||||
} else {
|
||||
sinceLastScheduledTaskDuration := time.Since(time.UnixMilli(janitorState.LastScheduledTaskRunUnixMilli))
|
||||
if !isRunningScheduledTask && sinceLastScheduledTaskDuration > time.Hour*24 {
|
||||
//go runScheduledTask(db, &config)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}
|
||||
|
||||
func runScheduledTask(db *DBModel, config *Config) {
|
||||
|
||||
isRunningScheduledTask = true
|
||||
log.Println("starting runScheduledTask...")
|
||||
|
||||
log.Println("GetDBTableSizes...")
|
||||
tables, err := db.GetDBTableSizes()
|
||||
if err != nil {
|
||||
log.Printf("ERROR!: runScheduledTask can't GetDBTableSizes: %s\n", err)
|
||||
}
|
||||
log.Println("Saving data/dbTableSizes.json...")
|
||||
err = WriteJsonFile[[]DBTableSize]("data/dbTableSizes.json", tables)
|
||||
if err != nil {
|
||||
log.Printf("ERROR!: runScheduledTask can't write data/dbTableSizes.json: %s\n", err)
|
||||
}
|
||||
|
||||
log.Println("GetAvaliableDiskSpace...")
|
||||
availableBytes, totalBytes, err := GetAvaliableDiskSpace(config.MediaFolder)
|
||||
if err != nil {
|
||||
log.Printf("ERROR!: runScheduledTask can't GetAvaliableDiskSpace: %s\n", err)
|
||||
}
|
||||
|
||||
log.Printf("GetTotalFilesizeWithinFolder(\"%s\")...\n", config.MediaFolder)
|
||||
mediaBytes, err := GetTotalFilesizeWithinFolder(config.MediaFolder)
|
||||
if err != nil {
|
||||
log.Printf("ERROR!: runScheduledTask can't GetTotalFilesizeWithinFolder(\"%s\"): %s\n", config.MediaFolder, err)
|
||||
}
|
||||
|
||||
log.Printf("GetTotalFilesizeWithinFolder(\"%s\")...\n", config.PostgresFolder)
|
||||
postgresBytes, err := GetTotalFilesizeWithinFolder(config.PostgresFolder)
|
||||
if err != nil {
|
||||
log.Printf("ERROR!: runScheduledTask can't GetTotalFilesizeWithinFolder(\"%s\"): %s\n", config.PostgresFolder, err)
|
||||
}
|
||||
|
||||
diskUsage := DiskUsage{
|
||||
DiskSizeBytes: totalBytes,
|
||||
OtherBytes: (totalBytes - availableBytes) - (mediaBytes + postgresBytes),
|
||||
MediaBytes: mediaBytes,
|
||||
PostgresBytes: postgresBytes,
|
||||
}
|
||||
|
||||
log.Println("Saving data/diskUsage.json...")
|
||||
err = WriteJsonFile[DiskUsage]("data/diskUsage.json", diskUsage)
|
||||
if err != nil {
|
||||
log.Printf("ERROR!: runScheduledTask can't write data/diskUsage.json: %s\n", err)
|
||||
}
|
||||
|
||||
log.Println("starting db.StateGroupsStateStream()...")
|
||||
stream, err := db.StateGroupsStateStream()
|
||||
if err != nil {
|
||||
log.Fatalf("Can't start because %+v\n", err)
|
||||
|
@ -41,25 +135,74 @@ func main() {
|
|||
updateCounter += 1
|
||||
rowCounter += 1
|
||||
if updateCounter > 10000 {
|
||||
if time.Now().After(lastUpdateTime.Add(time.Second)) {
|
||||
if time.Now().After(lastUpdateTime.Add(time.Second * 60)) {
|
||||
lastUpdateTime = time.Now()
|
||||
percent := int((float64(rowCounter) / float64(stream.EstimatedCount)) * float64(100))
|
||||
log.Printf("%d/%d (%d%s) ... \n", rowCounter, stream.EstimatedCount, percent, "%")
|
||||
log.Printf("state_groups_state table scan %d/%d (%d%s) ... \n", rowCounter, stream.EstimatedCount, percent, "%")
|
||||
}
|
||||
updateCounter = 0
|
||||
}
|
||||
}
|
||||
|
||||
output, err := json.MarshalIndent(rowCountByRoom, "", " ")
|
||||
|
||||
err = WriteJsonFile[map[string]int]("data/stateGroupsStateRowCountByRoom.json", rowCountByRoom)
|
||||
if err != nil {
|
||||
log.Printf("Can't save rooms.json because json.MarshalIndent returned %+v\n", err)
|
||||
log.Printf("ERROR!: runScheduledTask can't write data/stateGroupsStateRowCountByRoom.json: %s\n", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile("./rooms.json", output, 0755)
|
||||
log.Println("updating data/janitorState.json...")
|
||||
|
||||
janitorState, err := ReadJsonFile[JanitorState]("data/janitorState.json")
|
||||
if err != nil {
|
||||
log.Printf("Can't save rooms.json because os.WriteFile returned %+v\n", err)
|
||||
log.Printf("ERROR!: runScheduledTask can't read data/janitorState.json: %+v\n", err)
|
||||
}
|
||||
|
||||
janitorState.LastScheduledTaskRunUnixMilli = time.Now().UnixMilli()
|
||||
|
||||
err = WriteJsonFile[JanitorState]("data/janitorState.json", janitorState)
|
||||
if err != nil {
|
||||
log.Printf("ERROR!: runScheduledTask can't write data/janitorState.json: %s\n", err)
|
||||
}
|
||||
|
||||
log.Println("runScheduledTask completed!")
|
||||
isRunningScheduledTask = false
|
||||
}
|
||||
|
||||
func validateConfig(config *Config) {
|
||||
|
||||
errors := []string{}
|
||||
|
||||
if config.FrontendPort == 0 {
|
||||
errors = append(errors, "Can't start because FrontendPort is required")
|
||||
}
|
||||
if config.FrontendDomain == "" {
|
||||
errors = append(errors, "Can't start because FrontendDomain is required")
|
||||
}
|
||||
if config.MatrixURL == "" {
|
||||
errors = append(errors, "Can't start because MatrixURL is required")
|
||||
}
|
||||
if config.MatrixAdminToken == "" || config.MatrixAdminToken == "changeme" {
|
||||
errors = append(errors, "Can't start because MatrixAdminToken is required")
|
||||
}
|
||||
if config.MatrixServerPublicDomain == "" {
|
||||
errors = append(errors, "Can't start because MatrixServerPublicDomain is required")
|
||||
}
|
||||
if config.AdminMatrixRoomId == "" {
|
||||
errors = append(errors, "Can't start because AdminMatrixRoomId is required")
|
||||
}
|
||||
if config.DatabaseType == "" {
|
||||
errors = append(errors, "Can't start because DatabaseType is required")
|
||||
}
|
||||
if config.DatabaseConnectionString == "" {
|
||||
errors = append(errors, "Can't start because DatabaseConnectionString is required")
|
||||
}
|
||||
if config.MediaFolder == "" {
|
||||
errors = append(errors, "Can't start because MediaFolder is required")
|
||||
}
|
||||
if config.PostgresFolder == "" {
|
||||
errors = append(errors, "Can't start because PostgresFolder is required")
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
log.Fatalln(strings.Join(errors, "\n"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,9 +13,11 @@ import (
|
|||
)
|
||||
|
||||
type MatrixAdmin struct {
|
||||
Client http.Client
|
||||
URL string
|
||||
Token string
|
||||
Client http.Client
|
||||
AdminMatrixRoomId string
|
||||
MatrixServerPublicDomain string
|
||||
URL string
|
||||
Token string
|
||||
}
|
||||
|
||||
type DeleteRoomRequest struct {
|
||||
|
@ -45,14 +47,36 @@ type ShutdownRoom struct {
|
|||
NewRoomId string `json:"new_room_id"`
|
||||
}
|
||||
|
||||
type LoginRequestBody struct {
|
||||
Identifier LoginIdentifier `json:"identifier"`
|
||||
DeviceDisplayName string `json:"initial_device_display_name"`
|
||||
Password string `json:"password"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type LoginIdentifier struct {
|
||||
Type string `json:"type"`
|
||||
User string `json:"user"`
|
||||
}
|
||||
|
||||
type LoginResponseBody struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
|
||||
type RoomMembersResponseBody struct {
|
||||
Members []string `json:"members"`
|
||||
}
|
||||
|
||||
func initMatrixAdmin(config *Config) *MatrixAdmin {
|
||||
|
||||
return &MatrixAdmin{
|
||||
Client: http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
URL: config.MatrixURL,
|
||||
Token: config.MatrixAdminToken,
|
||||
AdminMatrixRoomId: config.AdminMatrixRoomId,
|
||||
MatrixServerPublicDomain: config.MatrixServerPublicDomain,
|
||||
URL: config.MatrixURL,
|
||||
Token: config.MatrixAdminToken,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -187,3 +211,77 @@ func (admin *MatrixAdmin) GetDeleteRoomStatus(roomId string) (string, []string,
|
|||
|
||||
return mostCompleteStatus, usersSlice, nil
|
||||
}
|
||||
|
||||
// curl 'https://matrix.cyberia.club/_matrix/client/r0/login' -X POST -H 'Accept: application/json' -H 'content-type: application/json'
|
||||
// --data-raw '{"type":"m.login.password","password":"xxxxxxxxx","identifier":{"type":"m.id.user","user":"forestjohnson"},"initial_device_display_name":"chat.cyberia.club (Firefox, Ubuntu)"}'
|
||||
|
||||
func (admin *MatrixAdmin) Login(username, password string) (bool, error) {
|
||||
|
||||
loginURL := fmt.Sprintf("%s/_matrix/client/v3/login", admin.URL)
|
||||
|
||||
loginRequestBodyObject := LoginRequestBody{
|
||||
Identifier: LoginIdentifier{
|
||||
Type: "m.id.user",
|
||||
User: username,
|
||||
},
|
||||
DeviceDisplayName: "matrix-synapse-diskspace-janitor",
|
||||
Password: password,
|
||||
Type: "m.login.password",
|
||||
}
|
||||
loginRequestBody, err := json.Marshal(loginRequestBodyObject)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "can't serialize LoginRequestBody to json")
|
||||
}
|
||||
loginResponse, err := admin.Client.Post(loginURL, "application/json", bytes.NewBuffer(loginRequestBody))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if loginResponse.StatusCode > 200 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
responseBody, err := ioutil.ReadAll(loginResponse.Body)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "HTTP POST %s read error", loginURL)
|
||||
}
|
||||
var responseObject LoginResponseBody
|
||||
err = json.Unmarshal(responseBody, &responseObject)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "HTTP POST %s response json parse error", loginURL)
|
||||
}
|
||||
|
||||
logoutURL := fmt.Sprintf("%s/_matrix/client/v3/logout", admin.URL)
|
||||
logoutRequest, err := http.NewRequest("POST", logoutURL, nil)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "matrixAdmin.Login(...) cannot create logoutRequest")
|
||||
}
|
||||
|
||||
logoutRequest.Header.Add("Authorization", fmt.Sprintf("Bearer %s", responseObject.AccessToken))
|
||||
|
||||
_, err = admin.Client.Do(logoutRequest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
roomMembersURLWithoutToken := fmt.Sprintf("%s/_synapse/admin/v1/rooms/%s/members", admin.URL, admin.AdminMatrixRoomId)
|
||||
roomMembersURL := fmt.Sprintf("%s%s", roomMembersURLWithoutToken, admin.Token)
|
||||
roomMembersResponse, err := admin.Client.Get(roomMembersURL)
|
||||
|
||||
roomMembersResponseBody, err := ioutil.ReadAll(roomMembersResponse.Body)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "HTTP POST %sxxxxxxx read error", roomMembersURLWithoutToken)
|
||||
}
|
||||
var roomMembersResponseObject RoomMembersResponseBody
|
||||
err = json.Unmarshal(roomMembersResponseBody, &roomMembersResponseObject)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "HTTP POST %sxxxxxxxxx response json parse error", roomMembersURLWithoutToken)
|
||||
}
|
||||
for _, member := range roomMembersResponseObject.Members {
|
||||
if member == fmt.Sprintf("@%s:%s", username, admin.MatrixServerPublicDomain) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
|
40
storage_service.go
Normal file
40
storage_service.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
errors "git.sequentialread.com/forest/pkg-errors"
|
||||
)
|
||||
|
||||
func WriteJsonFile[T any](path string, object T) error {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
return json.NewEncoder(file).Encode(object)
|
||||
}
|
||||
|
||||
func ReadJsonFile[T any](path string) (T, error) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
var object T
|
||||
file, err := os.OpenFile(path, os.O_RDONLY, 0644)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return object, nil
|
||||
}
|
||||
if err != nil {
|
||||
return object, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
err = json.NewDecoder(file).Decode(&object)
|
||||
if err != nil {
|
||||
return object, errors.Wrapf(err, "json parse error on %s", path)
|
||||
}
|
||||
return object, nil
|
||||
}
|
Loading…
Reference in a new issue