-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathusage.go
131 lines (108 loc) · 3.78 KB
/
usage.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package main
import (
"os"
"encoding/json"
"fmt"
"io/fs"
"strings"
"path/filepath"
"time"
"net/http"
)
type usageMetadata struct {
Total int64 `json:"total"`
}
const usageFileName = "..usage"
func readUsage(path string) (*usageMetadata, error) {
usage_path := filepath.Join(path, usageFileName)
usage_raw, err := os.ReadFile(usage_path)
if err != nil {
return nil, fmt.Errorf("failed to read '" + usage_path + "'; %w", err)
}
var output usageMetadata
err = json.Unmarshal(usage_raw, &output)
if err != nil {
return nil, fmt.Errorf("failed to parse JSON in '" + usage_path + "'; %w", err)
}
return &output, nil
}
func computeUsage(dir string, skip_symlinks bool) (int64, error) {
var total int64
total = 0
err := filepath.WalkDir(dir, func(path string, info fs.DirEntry, err error) error {
if err != nil {
return fmt.Errorf("failed to walk into %q; %w", path, err)
}
// Skipping internal files.
base := filepath.Base(path)
if strings.HasPrefix(base, "..") {
return nil
}
restat, err := info.Info()
if err != nil {
return fmt.Errorf("failed to stat %q; %w", path, err)
}
if restat.Mode() & os.ModeSymlink == os.ModeSymlink {
more_info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("failed to stat target of link %q; %w", path, err)
}
if more_info.IsDir() {
return fmt.Errorf("symlinks to directories are not supported (%q); %w", path, err)
}
if skip_symlinks {
return nil
}
total += more_info.Size()
} else {
if !info.IsDir() {
total += restat.Size()
}
}
return nil
})
return total, err
}
func refreshUsageHandler(reqpath string, globals *globalConfiguration) (*usageMetadata, error) {
source_user, err := identifyUser(reqpath)
if err != nil {
return nil, fmt.Errorf("failed to find owner of %q; %w", reqpath, err)
}
if !isAuthorizedToAdmin(source_user, globals.Administrators) {
return nil, newHttpError(http.StatusForbidden, fmt.Errorf("user %q is not authorized to refresh the latest version (%q)", source_user, reqpath))
}
incoming := struct {
Project *string `json:"project"`
}{}
{
handle, err := os.ReadFile(reqpath)
if err != nil {
return nil, fmt.Errorf("failed to read %q; %w", reqpath, err)
}
err = json.Unmarshal(handle, &incoming)
if err != nil {
return nil, newHttpError(http.StatusBadRequest, fmt.Errorf("failed to parse JSON from %q; %w", reqpath, err))
}
err = isMissingOrBadName(incoming.Project)
if err != nil {
return nil, newHttpError(http.StatusBadRequest, fmt.Errorf("invalid 'project' property in %q; %w", reqpath, err))
}
}
project_dir := filepath.Join(globals.Registry, *(incoming.Project))
err = globals.Locks.LockDirectory(project_dir, 10 * time.Second)
if err != nil {
return nil, fmt.Errorf("failed to lock the project directory %q; %w", project_dir, err)
}
defer globals.Locks.Unlock(project_dir)
new_usage, err := computeUsage(project_dir, true)
if err != nil {
return nil, fmt.Errorf("failed to compute usage for %q; %w", *(incoming.Project), err)
}
usage_path := filepath.Join(project_dir, usageFileName)
usage_meta := usageMetadata{ Total: new_usage }
err = dumpJson(usage_path, &usage_meta)
if err != nil {
return nil, fmt.Errorf("failed to write new usage for %q; %w", *(incoming.Project), err)
}
return &usage_meta, nil
}