It builds!
This commit is contained in:
@@ -38,7 +38,7 @@ var (
|
|||||||
adminEmail string
|
adminEmail string
|
||||||
|
|
||||||
// Limits
|
// Limits
|
||||||
resultsPerpage int
|
resultsPerPage int
|
||||||
|
|
||||||
// Secrets
|
// Secrets
|
||||||
apiSigningKey string
|
apiSigningKey string
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -29,7 +29,9 @@ require (
|
|||||||
github.com/prologic/bitcask v0.3.10
|
github.com/prologic/bitcask v0.3.10
|
||||||
github.com/prologic/observe v0.0.0-20181231082615-747b185a0928
|
github.com/prologic/observe v0.0.0-20181231082615-747b185a0928
|
||||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect
|
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect
|
||||||
|
github.com/renstrom/shortuuid v2.0.3+incompatible
|
||||||
github.com/robfig/cron v1.2.0
|
github.com/robfig/cron v1.2.0
|
||||||
|
github.com/satori/go.uuid v1.2.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.7.0
|
github.com/sirupsen/logrus v1.7.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/steambap/captcha v1.3.1
|
github.com/steambap/captcha v1.3.1
|
||||||
|
|||||||
5
go.sum
5
go.sum
@@ -240,12 +240,17 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
|
|||||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
|
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
|
||||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
|
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
|
||||||
|
github.com/renstrom/shortuuid v1.0.0 h1:RBZExpswQ58bdD70uCmUPhPfXNmICC+ir7IY4WNiCQ0=
|
||||||
|
github.com/renstrom/shortuuid v2.0.3+incompatible h1:wAE2LlEYCmNNyeLTzSrz3c9O8uNMkGckJj0Gzliqh9c=
|
||||||
|
github.com/renstrom/shortuuid v2.0.3+incompatible/go.mod h1:n18Ycpn8DijG+h/lLBQVnGKv1BCtTeXo8KKSbBOrQ8c=
|
||||||
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
||||||
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
||||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
|
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||||
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
|||||||
@@ -99,6 +99,14 @@ func (s *Server) IndexHandler() httprouter.Handle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddHandler ...
|
||||||
|
func (s *Server) AddHandler() httprouter.Handle {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
ctx := NewContext(s.config, s.db, r)
|
||||||
|
s.render("add", w, ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CachedHandler ...
|
// CachedHandler ...
|
||||||
func (s *Server) CachedHandler() httprouter.Handle {
|
func (s *Server) CachedHandler() httprouter.Handle {
|
||||||
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
|||||||
190
internal/manage_handlers.go
Normal file
190
internal/manage_handlers.go
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"github.com/renstrom/shortuuid"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ManageHandler ...
|
||||||
|
func (s *Server) ManageHandler() httprouter.Handle {
|
||||||
|
isAdminUser := IsAdminUserFactory(s.config)
|
||||||
|
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
|
ctx := NewContext(s.config, s.db, r)
|
||||||
|
|
||||||
|
if !isAdminUser(ctx.User) {
|
||||||
|
ctx.Error = true
|
||||||
|
ctx.Message = "You are not a Pod Owner!"
|
||||||
|
s.render("403", w, ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == "GET" {
|
||||||
|
s.render("managePod", w, ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
name := strings.TrimSpace(r.FormValue("podName"))
|
||||||
|
description := strings.TrimSpace(r.FormValue("podDescription"))
|
||||||
|
|
||||||
|
// Update name
|
||||||
|
if name != "" {
|
||||||
|
s.config.Name = name
|
||||||
|
} else {
|
||||||
|
ctx.Error = true
|
||||||
|
ctx.Message = ""
|
||||||
|
s.render("error", w, ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update pod description
|
||||||
|
if description != "" {
|
||||||
|
s.config.Description = description
|
||||||
|
} else {
|
||||||
|
ctx.Error = true
|
||||||
|
ctx.Message = ""
|
||||||
|
s.render("error", w, ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save config file
|
||||||
|
if err := s.config.Settings().Save(filepath.Join(s.config.Data, "settings.yaml")); err != nil {
|
||||||
|
log.WithError(err).Error("error saving config")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Error = false
|
||||||
|
ctx.Message = "Pod updated successfully"
|
||||||
|
s.render("error", w, ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ManageUsersHandler ...
|
||||||
|
func (s *Server) ManageUsersHandler() httprouter.Handle {
|
||||||
|
isAdminUser := IsAdminUserFactory(s.config)
|
||||||
|
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
|
ctx := NewContext(s.config, s.db, r)
|
||||||
|
|
||||||
|
if !isAdminUser(ctx.User) {
|
||||||
|
ctx.Error = true
|
||||||
|
ctx.Message = "You are not a Pod Owner!"
|
||||||
|
s.render("403", w, ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.render("manageUsers", w, ctx)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddUserHandler ...
|
||||||
|
func (s *Server) AddUserHandler() httprouter.Handle {
|
||||||
|
isAdminUser := IsAdminUserFactory(s.config)
|
||||||
|
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
ctx := NewContext(s.config, s.db, r)
|
||||||
|
|
||||||
|
if !isAdminUser(ctx.User) {
|
||||||
|
ctx.Error = true
|
||||||
|
ctx.Message = "You are not a Pod Owner!"
|
||||||
|
s.render("403", w, ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
username := NormalizeUsername(r.FormValue("username"))
|
||||||
|
// XXX: We DO NOT store this! (EVER)
|
||||||
|
email := strings.TrimSpace(r.FormValue("email"))
|
||||||
|
|
||||||
|
// Random password -- User is expected to user "Password Reset"
|
||||||
|
password := shortuuid.New()
|
||||||
|
|
||||||
|
if err := ValidateUsername(username); err != nil {
|
||||||
|
ctx.Error = true
|
||||||
|
ctx.Message = fmt.Sprintf("Username validation failed: %s", err.Error())
|
||||||
|
s.render("error", w, ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.db.HasUser(username) {
|
||||||
|
ctx.Error = true
|
||||||
|
ctx.Message = "User or Feed with that name already exists! Please pick another!"
|
||||||
|
s.render("error", w, ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := s.pm.CreatePassword(password)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Error("error creating password hash")
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
recoveryHash := fmt.Sprintf("email:%s", FastHash(email))
|
||||||
|
|
||||||
|
user := NewUser()
|
||||||
|
user.Username = username
|
||||||
|
user.Recovery = recoveryHash
|
||||||
|
user.Password = hash
|
||||||
|
user.CreatedAt = time.Now()
|
||||||
|
|
||||||
|
if err := s.db.SetUser(username, user); err != nil {
|
||||||
|
log.WithError(err).Error("error saving user object for new user")
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Error = false
|
||||||
|
ctx.Message = "User successfully created"
|
||||||
|
s.render("error", w, ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelUserHandler ...
|
||||||
|
func (s *Server) DelUserHandler() httprouter.Handle {
|
||||||
|
isAdminUser := IsAdminUserFactory(s.config)
|
||||||
|
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
ctx := NewContext(s.config, s.db, r)
|
||||||
|
|
||||||
|
if !isAdminUser(ctx.User) {
|
||||||
|
ctx.Error = true
|
||||||
|
ctx.Message = "You are not a Pod Owner!"
|
||||||
|
s.render("403", w, ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
username := NormalizeUsername(r.FormValue("username"))
|
||||||
|
|
||||||
|
user, err := s.db.GetUser(username)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Errorf("error loading user object for %s", username)
|
||||||
|
ctx.Error = true
|
||||||
|
ctx.Message = "Error deleting account"
|
||||||
|
s.render("error", w, ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete user
|
||||||
|
if err := s.db.DelUser(user.Username); err != nil {
|
||||||
|
ctx.Error = true
|
||||||
|
ctx.Message = "An error occured whilst deleting your account"
|
||||||
|
s.render("error", w, ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.sm.Delete(w, r)
|
||||||
|
|
||||||
|
ctx.Error = false
|
||||||
|
ctx.Message = "Successfully deleted account"
|
||||||
|
s.render("error", w, ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -149,7 +149,7 @@ func WithBaseURL(baseURL string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithAdminUser sets the Admin user used for granting special features to
|
// WithAdminUser sets the Admin username
|
||||||
func WithAdminUser(adminUser string) Option {
|
func WithAdminUser(adminUser string) Option {
|
||||||
return func(cfg *Config) error {
|
return func(cfg *Config) error {
|
||||||
cfg.AdminUser = adminUser
|
cfg.AdminUser = adminUser
|
||||||
@@ -157,6 +157,14 @@ func WithAdminUser(adminUser string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithAdminPass sets the Admin password
|
||||||
|
func WithAdminPass(adminPass string) Option {
|
||||||
|
return func(cfg *Config) error {
|
||||||
|
cfg.AdminPass = adminPass
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithAdminName sets the Admin name used to identify the pod operator
|
// WithAdminName sets the Admin name used to identify the pod operator
|
||||||
func WithAdminName(adminName string) Option {
|
func WithAdminName(adminName string) Option {
|
||||||
return func(cfg *Config) error {
|
return func(cfg *Config) error {
|
||||||
|
|||||||
@@ -300,10 +300,6 @@ func (s *Server) initRoutes() {
|
|||||||
s.router.GET("/add", s.AddHandler())
|
s.router.GET("/add", s.AddHandler())
|
||||||
s.router.POST("/add", s.AddHandler())
|
s.router.POST("/add", s.AddHandler())
|
||||||
|
|
||||||
s.router.GET("/settings", s.am.MustAuth(s.SettingsHandler()))
|
|
||||||
s.router.POST("/settings", s.am.MustAuth(s.SettingsHandler()))
|
|
||||||
s.router.POST("/token/delete/:signature", s.am.MustAuth(s.DeleteTokenHandler()))
|
|
||||||
|
|
||||||
s.router.GET("/manage", s.ManageHandler())
|
s.router.GET("/manage", s.ManageHandler())
|
||||||
s.router.POST("/manage", s.ManageHandler())
|
s.router.POST("/manage", s.ManageHandler())
|
||||||
|
|
||||||
@@ -357,7 +353,7 @@ func NewServer(bind string, options ...Option) (*Server, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tmplman, err := NewTemplateManager(config, blogs, cache)
|
tmplman, err := NewTemplateManager(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("error creating template manager")
|
log.WithError(err).Error("error creating template manager")
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -431,9 +427,6 @@ func NewServer(bind string, options ...Option) (*Server, error) {
|
|||||||
server.cron.Start()
|
server.cron.Start()
|
||||||
log.Info("started background jobs")
|
log.Info("started background jobs")
|
||||||
|
|
||||||
server.tasks.Start()
|
|
||||||
log.Info("started task dispatcher")
|
|
||||||
|
|
||||||
server.setupMetrics()
|
server.setupMetrics()
|
||||||
log.Infof("serving metrics endpoint at %s/metrics", server.config.BaseURL)
|
log.Infof("serving metrics endpoint at %s/metrics", server.config.BaseURL)
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ func (s *Server) ReportHandler() httprouter.Handle {
|
|||||||
|
|
||||||
if r.Method == "GET" {
|
if r.Method == "GET" {
|
||||||
ctx.Title = "Report abuse"
|
ctx.Title = "Report abuse"
|
||||||
ctx.ReportNick = nick
|
|
||||||
ctx.ReportURL = url
|
ctx.ReportURL = url
|
||||||
s.render("report", w, ctx)
|
s.render("report", w, ctx)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -30,6 +31,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
admin = "admin"
|
||||||
|
maxUsernameLength = 15 // avg 6 chars / 2 syllables per name commonly
|
||||||
|
|
||||||
CacheDir = "cache"
|
CacheDir = "cache"
|
||||||
|
|
||||||
requestTimeout = time.Second * 30
|
requestTimeout = time.Second * 30
|
||||||
@@ -41,7 +45,16 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
reservedUsernames = []string{
|
||||||
|
admin,
|
||||||
|
}
|
||||||
|
|
||||||
|
validUsername = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_-]+$`)
|
||||||
|
|
||||||
ErrBadRequest = errors.New("error: request failed with non-200 response")
|
ErrBadRequest = errors.New("error: request failed with non-200 response")
|
||||||
|
ErrInvalidUsername = errors.New("error: invalid username")
|
||||||
|
ErrUsernameTooLong = errors.New("error: username is too long")
|
||||||
|
ErrReservedUsername = errors.New("error: username is reserved")
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateRandomToken() string {
|
func GenerateRandomToken() string {
|
||||||
@@ -370,6 +383,29 @@ func SafeParseInt(s string, d int) int {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateUsername validates the username before allowing it to be created.
|
||||||
|
// This ensures usernames match a defined pattern and that some usernames
|
||||||
|
// that are reserved are never used by users.
|
||||||
|
func ValidateUsername(username string) error {
|
||||||
|
username = NormalizeUsername(username)
|
||||||
|
|
||||||
|
if !validUsername.MatchString(username) {
|
||||||
|
return ErrInvalidUsername
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, reservedUsername := range reservedUsernames {
|
||||||
|
if username == reservedUsername {
|
||||||
|
return ErrReservedUsername
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(username) > maxUsernameLength {
|
||||||
|
return ErrUsernameTooLong
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func FormatForDateTime(t time.Time) string {
|
func FormatForDateTime(t time.Time) string {
|
||||||
var format string
|
var format string
|
||||||
|
|
||||||
|
|||||||
0
spyda.db/000000000.data
Normal file
0
spyda.db/000000000.data
Normal file
0
spyda.db/000000002.data
Normal file
0
spyda.db/000000002.data
Normal file
1
spyda.db/config.json
Normal file
1
spyda.db/config.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"max_datafile_size":1048576,"max_key_size":256,"max_value_size":65536,"sync":false,"autorecovery":false,"DirFileModeBeforeUmask":448,"FileFileModeBeforeUmask":384}
|
||||||
0
spyda.db/index
Normal file
0
spyda.db/index
Normal file
1
spyda.db/meta.json
Executable file
1
spyda.db/meta.json
Executable file
@@ -0,0 +1 @@
|
|||||||
|
{"index_up_to_date":true,"reclaimable_space":0}
|
||||||
Reference in New Issue
Block a user