-
Notifications
You must be signed in to change notification settings - Fork 1
/
appdir.go
203 lines (174 loc) · 5.56 KB
/
appdir.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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
/*
© 2021–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
ISC License
*/
package pos
import (
"os"
"path/filepath"
"sync/atomic"
"unicode"
"github.com/haraldrudell/parl/perrors"
"github.com/haraldrudell/parl/pfs"
"github.com/haraldrudell/parl/punix"
)
const (
// path segment “.local”.
// “~/.local/share” is a standardized directory on Linux
dotLocalDir = ".local"
// path segement “share”.
// “~/.local/share” is a standardized directory on Linux
shareDir = "share"
// mode for created directories
urwx os.FileMode = 0700
)
// for testing
var homeDirHook string
// AppDirectory manages a per-user writable app-specific directory
type AppDirectory struct {
// app name like “myapp”
App string
// absolute clean symlink-free path if app-directory exists
// - macOS: “/Users/user/.local/share/myapp”
// - Linux: “/home/user/.local/share/myapp”
abs atomic.Pointer[string]
}
// NewAppDir returns a writable directory object in the user’s home directory
// - appName: application name like “myapp”
// Unicode letters and digits
// - directory is “~/.local/share/[appName]”
// - parent directory is based on the running process’ owner
// - does not rely on environment variables
//
// Usage:
//
// var appDir = NewAppDir("myapp")
// if err = appDir.EnsureDir(); err != nil {…
// var knownToExistAbsCleanNoSymlinksNeverErrors = appDir.Directory()
func NewAppDir(appName string) (appd *AppDirectory) { return &AppDirectory{App: appName} }
// best-effort single-value absolute clean possibly symlink-free directory
// - returns an absolute path whether the directory exists or not
// - if directory exists, absolute clean symlink-free, otherwise absolute clean
// - Directory may panic from errors that are returned by [AppDirectory.EnsureDir] or
// [AppDirectory.Path].
// To avoid panics, invoke those methods first.
//
// Usage:
//
// var dir = NewAppDir("myapp").Directory()
func (d *AppDirectory) Directory() (abs string) {
var isNotExist bool
var err error
if abs, isNotExist, err = d.Path(); err != nil && !isNotExist {
panic(err) // some error
}
return
}
// EnsureDir ensures the directory exists
func (d *AppDirectory) EnsureDir() (err error) {
// get path while checking if already exists
var abs string
var isNotExist bool
if abs, isNotExist, err = d.Path(); err == nil {
return // directory already exists return
} else if !isNotExist {
return // some error
}
// MkDirAll begins with stat to see if path exists
if err = os.MkdirAll(abs, urwx); perrors.IsPF(&err, "os.MkdirAll: %w", err) {
return
}
// update d.abs
_, _, err = d.eval(abs)
return
}
// Path returns best-effort absolute clean path
// - if the app-directory exists, abs is also symlink-free
// - outcomes:
// - — err: nil: abs is absolute clean symlink-free, app directory exists
// - — isNotExist: true, err: non-nil: app directory does not eixst.
// abs is absolute clean.
// err is errno ENOENT
// - — err: non-nil, isNotExist: false: some error
// - —
// - macOS: “/Users/user/.local/share/myapp”
// - Linux: “/home/user/.local/share/myapp”
// - note: symlinks can only be evaled if a path exists
func (d *AppDirectory) Path() (abs string, isNotExist bool, err error) {
// if already present
if ap := d.abs.Load(); ap != nil {
abs = *ap
return // success: already has abs, directory exists return
}
// check appName
var appName string
if appName, err = d.checkAppName(); err != nil {
return // bad appName return
}
// get user’s home directory
var homeDir string
if h := homeDirHook; h == "" {
if homeDir, err = UserHome(); err != nil {
return // failure to obtain home directory return
}
} else {
homeDir = h
}
// get app directory’s parent
// - absolute, maybe unclean, maybe symlinks
var parentDir = filepath.Join(homeDir, dotLocalDir, shareDir)
// get app directory
// - absolute, maybe unclean, maybe symlinks
var a = filepath.Join(parentDir, appName)
// try to unsymlink app directory
if abs, isNotExist, err = d.eval(a); err == nil {
return // app directory exists success return
} else if !isNotExist {
return // some error return
}
// err is non-nil, isNotExist true
// try to unsymlink parent directory
if p, e := pfs.AbsEval(parentDir); e != nil {
if punix.IsENOENT(e) {
abs = a
return // parent no exist either, return isNotExist result
}
err = e // some new error
isNotExist = false
return // return error from parent directory
} else {
// use th evealed parent directory
abs = filepath.Clean(filepath.Join(p, appName))
}
return // parent exists, app dir does not isNotExist return
}
// checks that appName is usable
func (d *AppDirectory) checkAppName() (appName string, err error) {
if appName = d.App; appName == "" {
err = perrors.NewPF("appName cannot be empty")
return // empty error return
}
for i, c := range appName {
if !unicode.IsDigit(c) && !unicode.IsLetter(c) {
err = perrors.ErrorfPF(
"appName can only contain Unicode letters or digits: #%d: %q",
i, c,
)
return // bad character error return
}
}
return // good return
}
// eval evaluates the full app directory path
// - on success, updates d.abs
func (d *AppDirectory) eval(path string) (abs string, isNotExist bool, err error) {
var a string
if a, err = pfs.AbsEval(path); err != nil {
isNotExist = punix.IsENOENT(err)
return // some error including does not exist
}
// success, app directory exists and is evaled
d.abs.CompareAndSwap(nil, &a)
abs = a
return // success, directory exists
}