Skip to content

Commit

Permalink
Provide richer data about disk space usage (#537)
Browse files Browse the repository at this point in the history
  • Loading branch information
mtlynch authored Feb 10, 2024
1 parent 2cd9979 commit 13c1486
Show file tree
Hide file tree
Showing 12 changed files with 691 additions and 33 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,16 @@ Due to time limitations, I keep PicoShare's scope limited to only the features t
PicoShare is easy to deploy to cloud hosting platforms:

- [fly.io](docs/deployment/fly.io.md)

## Tips and tricks

### Reclaiming reserved database space

Some users find it surprising that when they delete files from PicoShare, they don't gain back free space on their filesystem.

When you delete files, PicoShare reserves the space for future uploads. If you'd like to reduce PicoShare's usage of your filesystem, you can manually force PicoShare to give up the space by performing the following steps:

1. Shut down PicoShare.
1. Run `sqlite3 data/store.db 'VACUUM'` where `data/store.db` is the path to your PicoShare database.

You should find that the `data/store.db` should shrink in file size, as it relinquishes the space dedicated to previously deleted files. If you start PicoShare again, the System Information screen will show the smaller size of PicoShare files.
2 changes: 1 addition & 1 deletion cmd/picoshare/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func main() {

store := sqlite.New(*dbPath, isLitestreamEnabled())

spaceChecker := space.NewChecker(dbDir)
spaceChecker := space.NewChecker(*dbPath, &store)

collector := garbagecollect.NewCollector(store)
gc := garbagecollect.NewScheduler(&collector, 7*time.Hour)
Expand Down
2 changes: 1 addition & 1 deletion handlers/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

type (
SpaceChecker interface {
Check() (space.CheckResult, error)
Check() (space.Usage, error)
}

Server struct {
Expand Down
4 changes: 4 additions & 0 deletions handlers/static/js/lib/bulma.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ export function hideElement(el) {
export function showElement(el) {
el.classList.remove("is-hidden");
}

export function toggleShowElement(el) {
el.classList.toggle("is-hidden");
}
49 changes: 44 additions & 5 deletions handlers/templates/pages/system-information.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,24 @@
{{ end }}

{{ define "script-tags" }}
<script type="module" nonce="{{ .CspNonce }}">
import { toggleShowElement, hideElement } from "/js/lib/bulma.js";

const sizeDeltaNotification = document.querySelector(".notification");

document
.getElementById("file-size-information")
.addEventListener("click", (evt) => {
toggleShowElement(sizeDeltaNotification);
});

document.querySelectorAll(".notification .delete").forEach((el) => {
el.addEventListener("click", (evt) => {
const notificationEl = evt.target.parentElement;
hideElement(notificationEl);
});
});
</script>
{{ end }}

{{ define "custom-elements" }}
Expand All @@ -38,18 +56,39 @@ <h2>Disk Usage</h2>
<strong>Used space</strong>:
{{ formatFileSize .UsedBytes }}
({{ percentage .UsedBytes .TotalBytes }})
<ul>
<li>
<strong>Upload data</strong>:
{{ formatFileSize .TotalServingBytes }}
({{ percentage .TotalServingBytes .UsedBytes }})
</li>
<li>
<strong>PicoShare files</strong>:
{{ formatFileSize .DatabaseFileBytes }}
({{ percentage .DatabaseFileBytes .UsedBytes }})
<a id="file-size-information" href="#"
><span class="icon has-text-info">
<i class="fas fa-info-circle"></i> </span
></a>
</li>
</ul>
</li>

<li><strong>Total disk space</strong>: {{ formatFileSize .TotalBytes }}</li>
</ul>

<div class="notification is-info is-light">
<div class="notification is-info is-light is-hidden">
<button class="delete"></button>
<p>
<strong>Note</strong>: Your used space may be higher than the sum total of
your files.
PicoShare occupies more disk space than the sum total of its file uploads.
</p>
<p>
When you delete files, PicoShare retains the space and reuses it to store
future uploads.
When you delete files from PicoShare, it retains the space and reuses it
to store future uploads. To manually reclaim reserved disk space, see the
<a
href="https://github.com/mtlynch/picoshare?tab=readme-ov-file#reclaiming-reserved-database-space"
>README</a
>.
</p>
</div>

Expand Down
24 changes: 14 additions & 10 deletions handlers/views.go
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ func (s Server) settingsGet() http.HandlerFunc {

func (s Server) systemInformationGet() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
space, err := s.spaceChecker.Check()
spaceUsage, err := s.spaceChecker.Check()
if err != nil {
log.Printf("error checking available space: %v", err)
http.Error(w, fmt.Sprintf("failed to check available space: %v", err), http.StatusInternalServerError)
Expand All @@ -562,16 +562,20 @@ func (s Server) systemInformationGet() http.HandlerFunc {

if err := renderTemplate(w, "system-information.html", struct {
commonProps
UsedBytes uint64
TotalBytes uint64
BuildTime time.Time
Version string
TotalServingBytes uint64
DatabaseFileBytes uint64
UsedBytes uint64
TotalBytes uint64
BuildTime time.Time
Version string
}{
commonProps: makeCommonProps("PicoShare - System Information", r.Context()),
UsedBytes: space.TotalBytes - space.AvailableBytes,
TotalBytes: space.TotalBytes,
BuildTime: build.Time(),
Version: build.Version,
commonProps: makeCommonProps("PicoShare - System Information", r.Context()),
TotalServingBytes: spaceUsage.TotalServingBytes,
DatabaseFileBytes: spaceUsage.DatabaseFileSize,
UsedBytes: spaceUsage.FileSystemUsedBytes,
TotalBytes: spaceUsage.FileSystemTotalBytes,
BuildTime: build.Time(),
Version: build.Version,
}, template.FuncMap{
"formatFileSize": humanReadableFileSize,
"percentage": func(part, total uint64) string {
Expand Down
65 changes: 49 additions & 16 deletions space/checker.go
Original file line number Diff line number Diff line change
@@ -1,32 +1,65 @@
package space

import (
"golang.org/x/sys/unix"
)
import "github.com/mtlynch/picoshare/v2/space/checkers"

type (
FileSystemChecker interface {
MeasureUsage() (checkers.PicoShareUsage, error)
}

DatabaseChecker interface {
TotalSize() (uint64, error)
}

Checker struct {
dataDir string
fsChecker FileSystemChecker
dbChecker DatabaseChecker
}

CheckResult struct {
AvailableBytes uint64
TotalBytes uint64
Usage struct {
// TotalServingBytes represents the sum total of the bytes of file data that
// PicoShare has of file uploads in the database. This is just file bytes
// and does not include PicoShare-specific metadata about the files.
TotalServingBytes uint64
// DatabaseFileSize represents the total number of bytes on the filesystem
// dedicated to storing PicoShare's SQLite database files.
DatabaseFileSize uint64
// FileSystemUsedBytes represents total bytes in use on the filesystem where
// PicoShare's database files are located. This represents the total of all
// used bytes on the filesystem, not just PicoShare.
FileSystemUsedBytes uint64
// FileSystemTotalBytes represents the total bytes available on the
// filesystem where PicoShare's database files are located.
FileSystemTotalBytes uint64
}
)

func NewChecker(dir string) Checker {
return Checker{dir}
func NewChecker(dbPath string, dbReader checkers.DatabaseMetadataReader) Checker {
return NewCheckerFromCheckers(checkers.NewFileSystemChecker(dbPath), checkers.NewDatabaseChecker(dbReader))
}

func (c Checker) Check() (CheckResult, error) {
var stat unix.Statfs_t
if err := unix.Statfs(c.dataDir, &stat); err != nil {
return CheckResult{}, err
func NewCheckerFromCheckers(fsChecker FileSystemChecker, dbChecker DatabaseChecker) Checker {
return Checker{
fsChecker,
dbChecker,
}
}

func (c Checker) Check() (Usage, error) {
fsUsage, err := c.fsChecker.MeasureUsage()
if err != nil {
return Usage{}, err
}

dbTotalSize, err := c.dbChecker.TotalSize()
if err != nil {
return Usage{}, err
}

return CheckResult{
AvailableBytes: stat.Bfree * uint64(stat.Bsize),
TotalBytes: stat.Blocks * uint64(stat.Bsize),
return Usage{
TotalServingBytes: dbTotalSize,
DatabaseFileSize: fsUsage.PicoShareDbFileSize,
FileSystemUsedBytes: fsUsage.UsedBytes,
FileSystemTotalBytes: fsUsage.TotalBytes,
}, nil
}
106 changes: 106 additions & 0 deletions space/checker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package space_test

import (
"errors"
"testing"

"github.com/mtlynch/picoshare/v2/space"
"github.com/mtlynch/picoshare/v2/space/checkers"
)

type mockFileSystemChecker struct {
usage checkers.PicoShareUsage
err error
}

func (c mockFileSystemChecker) MeasureUsage() (checkers.PicoShareUsage, error) {
return c.usage, c.err
}

type mockDatabaseChecker struct {
totalSize uint64
err error
}

func (c mockDatabaseChecker) TotalSize() (uint64, error) {
return c.totalSize, c.err
}

func TestCheck(t *testing.T) {
dummyFileSystemErr := errors.New("dummy filesystem checker error")
dummyDatabaseErr := errors.New("dummy database checker error")
for _, tt := range []struct {
description string
fsUsage checkers.PicoShareUsage
fsErr error
dbUsage uint64
dbErr error
usageExpected space.Usage
errExpected error
}{
{
description: "aggregates checker results correctly",
fsUsage: checkers.PicoShareUsage{
FileSystemUsage: checkers.FileSystemUsage{
UsedBytes: 70,
TotalBytes: 100,
},
PicoShareDbFileSize: 65,
},
fsErr: nil,
dbUsage: 60,
dbErr: nil,
usageExpected: space.Usage{
TotalServingBytes: 60,
DatabaseFileSize: 65,
FileSystemUsedBytes: 70,
FileSystemTotalBytes: 100,
},
errExpected: nil,
},
{
description: "returns error when filesystem checker fails",
fsUsage: checkers.PicoShareUsage{},
fsErr: dummyFileSystemErr,
dbUsage: 5,
dbErr: nil,
usageExpected: space.Usage{},
errExpected: dummyFileSystemErr,
},
{
description: "returns error when database checker fails",
fsUsage: checkers.PicoShareUsage{
PicoShareDbFileSize: 3,
FileSystemUsage: checkers.FileSystemUsage{
UsedBytes: 5,
TotalBytes: 7,
},
},
fsErr: nil,
dbUsage: 0,
dbErr: dummyDatabaseErr,
usageExpected: space.Usage{},
errExpected: dummyDatabaseErr,
},
} {
t.Run(tt.description, func(t *testing.T) {
fsc := mockFileSystemChecker{
usage: tt.fsUsage,
err: tt.fsErr,
}
dbc := mockDatabaseChecker{
totalSize: tt.dbUsage,
err: tt.dbErr,
}

usage, err := space.NewCheckerFromCheckers(fsc, dbc).Check()
if got, want := err, tt.errExpected; got != want {
t.Fatalf("err=%v, want=%v", got, want)
}

if got, want := usage, tt.usageExpected; got != want {
t.Errorf("usage=%+v, want=%+v", got, want)
}
})
}
}
Loading

0 comments on commit 13c1486

Please sign in to comment.