-
Notifications
You must be signed in to change notification settings - Fork 0
/
docdb.go
440 lines (364 loc) · 16.5 KB
/
docdb.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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
package docdb
import (
"context"
"github.com/ungerik/go-fs"
"github.com/ungerik/go-fs/uuiddir"
"github.com/domonda/go-errs"
"github.com/domonda/go-types/uu"
)
// DocumentExists returns true if a document with the passed docID exists
func DocumentExists(ctx context.Context, docID uu.ID) (exists bool, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID)
return conn.DocumentExists(ctx, docID)
}
// EnumDocumentIDs calls the passed callback with the ID of every document in the database
func EnumDocumentIDs(ctx context.Context, callback func(context.Context, uu.ID) error) (err error) {
defer errs.WrapWithFuncParams(&err, ctx)
return conn.EnumDocumentIDs(ctx, callback)
}
// EnumCompanyDocumentIDs calls the passed callback with the ID of every document of a company in the database
func EnumCompanyDocumentIDs(ctx context.Context, companyID uu.ID, callback func(context.Context, uu.ID) error) (err error) {
defer errs.WrapWithFuncParams(&err, ctx, companyID)
return conn.EnumCompanyDocumentIDs(ctx, companyID, callback)
}
// DocumentCompanyID returns the companyID for a docID
func DocumentCompanyID(ctx context.Context, docID uu.ID) (companyID uu.ID, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID)
return conn.DocumentCompanyID(ctx, docID)
}
// SetDocumentCompanyID changes the companyID for a document
func SetDocumentCompanyID(ctx context.Context, docID, companyID uu.ID) (err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID, companyID)
return conn.SetDocumentCompanyID(ctx, docID, companyID)
}
// DocumentVersions returns all version timestamps of a document in ascending order.
// Returns nil and no error if the document does not exist or has no versions.
func DocumentVersions(ctx context.Context, docID uu.ID) (versions []VersionTime, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID)
return conn.DocumentVersions(ctx, docID)
}
// LatestDocumentVersion returns the lates VersionTime of a document
func LatestDocumentVersion(ctx context.Context, docID uu.ID) (version VersionTime, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID)
return conn.LatestDocumentVersion(ctx, docID)
}
// DocumentVersionInfo returns the VersionInfo for a VersionTime
func DocumentVersionInfo(ctx context.Context, docID uu.ID, version VersionTime) (info *VersionInfo, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID, version)
return conn.DocumentVersionInfo(ctx, docID, version)
}
// LatestDocumentVersionInfo returns the VersionInfo for the latest document version
func LatestDocumentVersionInfo(ctx context.Context, docID uu.ID) (info *VersionInfo, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID)
return conn.LatestDocumentVersionInfo(ctx, docID)
}
// DocumentVersionFileProvider returns a FileProvider for the files of a document version
func DocumentVersionFileProvider(ctx context.Context, docID uu.ID, version VersionTime) (p FileProvider, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID, version)
return conn.DocumentVersionFileProvider(ctx, docID, version)
}
// ReadDocumentFile reads a file of the latest document version
func ReadDocumentFile(ctx context.Context, docID uu.ID, filename string) (data []byte, versionInfo *VersionInfo, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID, filename)
versionInfo, err = conn.LatestDocumentVersionInfo(ctx, docID)
if err != nil {
return nil, nil, err
}
data, err = conn.ReadDocumentVersionFile(ctx, docID, versionInfo.Version, filename)
if err != nil {
return nil, nil, err
}
return data, versionInfo, nil
}
// SubstituteDeletedDocumentVersion will substitue the passed version with
// the next existing version it does not exist anymore.
// Will return ErrDocumentHasNoCommitedVersion if there is no
// other commited version for the document.
func SubstituteDeletedDocumentVersion(ctx context.Context, docID uu.ID, version VersionTime) (validVersion VersionTime, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID, version)
_, err = conn.DocumentVersionInfo(ctx, docID, version)
if err == nil {
return version, nil
}
if !errs.Has[ErrDocumentVersionNotFound](err) {
return VersionTime{}, err
}
versions, err := conn.DocumentVersions(ctx, docID)
if err != nil {
return VersionTime{}, err
}
if len(versions) == 0 {
return VersionTime{}, NewErrDocumentHasNoCommitedVersion(docID)
}
for i := range versions {
// Return the first version after the deleted one
if versions[i].Time.After(version.Time) {
return versions[i], nil
}
}
// Return latest vesion if none is after the deleted one
return versions[len(versions)-1], nil
}
// ReadDocumentVersionFile returns the contents of a file of a document version.
// Wrapped ErrDocumentNotFound, ErrDocumentVersionNotFound, ErrDocumentFileNotFound
// will be returned in case of such error conditions.
func ReadDocumentVersionFile(ctx context.Context, docID uu.ID, version VersionTime, filename string) (data []byte, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID, version, filename)
return conn.ReadDocumentVersionFile(ctx, docID, version, filename)
}
func ReadLatestDocumentVersionFile(ctx context.Context, docID uu.ID, filename string) (data []byte, version VersionTime, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID, version, filename)
version, err = conn.LatestDocumentVersion(ctx, docID)
if err != nil {
return nil, VersionTime{}, err
}
data, err = conn.ReadDocumentVersionFile(ctx, docID, version, filename)
if err != nil {
return nil, VersionTime{}, err
}
return data, version, nil
}
// DocumentVersionFileReader returns a fs.FileReader for a file of a document version.
// Wrapped ErrDocumentNotFound, ErrDocumentVersionNotFound, ErrDocumentFileNotFound
// will be returned in case of such error conditions.
func DocumentVersionFileReader(ctx context.Context, docID uu.ID, version VersionTime, filename string) (fileReader fs.FileReader, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID, version, filename)
data, err := conn.ReadDocumentVersionFile(ctx, docID, version, filename)
if err != nil {
return nil, err
}
return fs.NewMemFile(filename, data), nil
}
// DocumentFileReader returns a fs.FileReader for a file of the latest document version.
// Wrapped ErrDocumentNotFound, ErrDocumentHasNoCommitedVersion, ErrDocumentFileNotFound
// will be returned in case of such error conditions.
func DocumentFileReader(ctx context.Context, docID uu.ID, filename string) (fileReader fs.FileReader, versionInfo *VersionInfo, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID, filename)
versionInfo, err = conn.LatestDocumentVersionInfo(ctx, docID)
if err != nil {
return nil, nil, err
}
data, err := conn.ReadDocumentVersionFile(ctx, docID, versionInfo.Version, filename)
if err != nil {
return nil, nil, err
}
return fs.NewMemFile(filename, data), versionInfo, nil
}
// DocumentFileExists returns if a document file with filename exists in the latest document version.
func DocumentFileExists(ctx context.Context, docID uu.ID, filename string) (exists bool, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID, filename)
file, _, err := DocumentFileReader(ctx, docID, filename)
if errs.Has[ErrDocumentFileNotFound](err) {
return false, nil
}
if err != nil {
return false, err
}
return file.Exists(), nil
}
// DocumentCheckOutStatus returns the CheckOutStatus of a document.
// If the document is not checked out, then a nil CheckOutStatus will be returned.
// The methods Valid() and String() can be called on a nil CheckOutStatus.
// ErrDocumentNotFound is returned if the document does not exist.
func DocumentCheckOutStatus(ctx context.Context, docID uu.ID) (status *CheckOutStatus, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID)
return conn.DocumentCheckOutStatus(ctx, docID)
}
// CheckedOutDocumentDir returns a fs.File for the directory
// where a document would be checked out.
func CheckedOutDocumentDir(docID uu.ID) fs.File {
return conn.CheckedOutDocumentDir(docID)
}
// CheckedOutDocumentFileProvider returns a FileProvider for the directory
// where a document would be checked out.
func CheckedOutDocumentFileProvider(docID uu.ID) (p FileProvider, err error) {
defer errs.WrapWithFuncParams(&err, docID)
checkOutDir := conn.CheckedOutDocumentDir(docID)
if !checkOutDir.Exists() {
return nil, NewErrDocumentNotCheckedOut(docID)
}
return DirFileProvider(checkOutDir), nil
}
// CancelCheckOutDocument cancels a potential checkout.
// No error is returned if the document was not checked out.
// If the checkout was created by CheckOutNewDocument,
// then the new document is deleted without leaving any history
// and the returned lastVersion.IsNull() is true.
func CancelCheckOutDocument(ctx context.Context, docID uu.ID) (wasCheckedOut bool, lastVersion VersionTime, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID)
return conn.CancelCheckOutDocument(ctx, docID)
}
// CheckInDocument checks in a checked out document
// and returns the VersionInfo for the newly created version.
func CheckInDocument(ctx context.Context, docID uu.ID) (v *VersionInfo, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID)
return conn.CheckInDocument(ctx, docID)
}
// CheckedOutDocuments returns the CheckOutStatus of all checked out documents.
func CheckedOutDocuments(ctx context.Context) (stati []*CheckOutStatus, err error) {
defer errs.WrapWithFuncParams(&err, ctx)
return conn.CheckedOutDocuments(ctx)
}
// CheckOutNewDocument creates a new document for a company in checked out state.
func CheckOutNewDocument(ctx context.Context, docID, companyID, userID uu.ID, reason string) (status *CheckOutStatus, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID, companyID, userID, reason)
return conn.CheckOutNewDocument(ctx, docID, companyID, userID, reason)
}
// CheckOutDocument checks out a document for a user with a stated reason.
// Returns ErrDocumentCheckedOut if the document is already checked out.
func CheckOutDocument(ctx context.Context, docID, userID uu.ID, reason string) (status *CheckOutStatus, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID, userID, reason)
return conn.CheckOutDocument(ctx, docID, userID, reason)
}
// DeleteDocument deletes all versions of a document
// including its workspace directory if checked out.
func DeleteDocument(ctx context.Context, docID uu.ID) (err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID)
return conn.DeleteDocument(ctx, docID)
}
// DeleteDocumentVersion deletes a version of a document that must not be checked out
// and returns the left over versions.
// If the version is the only version of the document,
// then the document will be deleted and no leftVersions are returned.
// Returns wrapped ErrDocumentNotFound, ErrDocumentVersionNotFound, ErrDocumentCheckedOut
// in case of such error conditions.
// DeleteDocumentVersion should not be used for normal docdb operations,
// just to clean up mistakes or sync database states.
func DeleteDocumentVersion(ctx context.Context, docID uu.ID, version VersionTime) (leftVersions []VersionTime, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID, version)
return conn.DeleteDocumentVersion(ctx, docID, version)
}
// InsertDocumentVersion inserts a new version for an existing document.
// Returns wrapped ErrDocumentNotFound, ErrDocumentVersionAlreadyExists
// in case of such error conditions.
// InsertDocumentVersion should not be used for normal docdb operations,
// just to clean up mistakes or sync database states.
// func InsertDocumentVersion(ctx context.Context, docID uu.ID, version VersionTime, userID uu.ID, reason string, files []fs.FileReader) (info *VersionInfo, err error) {
// defer errs.WrapWithFuncParams(&err, ctx, docID, version, userID, reason, files)
// return conn.InsertDocumentVersion(ctx, docID, version, userID, reason, files)
// }
func CreateDocument(ctx context.Context, companyID, docID, userID uu.ID, reason string, files []fs.FileReader) (versionInfo *VersionInfo, err error) {
defer errs.WrapWithFuncParams(&err, ctx, companyID, docID, userID, reason, files)
return conn.CreateDocument(ctx, companyID, docID, userID, reason, files)
}
// AddDocumentVersion adds a new version to a document
// using the files returned by createVersion.
// Returns a wrapped ErrNoChanges if the files returned by
// createVersion are the same as the latest version.
func AddDocumentVersion(ctx context.Context, docID, userID uu.ID, reason string, createVersion CreateVersionFunc, onNewVersion OnNewVersionFunc) (err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID, userID, reason)
return conn.AddDocumentVersion(ctx, docID, userID, reason, createVersion, onNewVersion)
}
// CopyDocumentFiles copies the files of all versions of
// a document to a backup directory.
//
// The backupDir must exist and a directory structure for the docID
// will be created inside the backupDir and returned as docDir.
//
// If true is passed for overwrite then existing files will be overwritten
// else an error is reeturned when docDir already exists.
//
// In case of an error the already created directories and files will be removed.
func CopyDocumentFiles(ctx context.Context, conn Conn, docID uu.ID, backupDir fs.File, overwrite bool) (destDocDir fs.File, err error) {
defer errs.WrapWithFuncParams(&err, ctx, docID, backupDir, overwrite)
destDocDir = uuiddir.Join(backupDir, docID)
if !overwrite && destDocDir.Exists() {
return "", errs.Errorf("document directory for backup already exists: %s", destDocDir)
}
// Better not do that because there might be existing files from before that should not get deleted:
// defer func() {
// if err != nil {
// // Remove created files and directories in case of an error
// err = errors.Join(err, uuiddir.RemoveDir(backupDir, docDir))
// }
// }()
log.InfoCtx(ctx, "Backing up document").
UUID("docID", docID).
Stringer("destDocDir", destDocDir).
Bool("overwrite", overwrite).
Log()
versions, err := conn.DocumentVersions(ctx, docID)
if err != nil {
return "", err
}
if len(versions) == 0 {
return "", NewErrDocumentHasNoCommitedVersion(docID)
}
if !destDocDir.Exists() {
log.Debug("Making directory").Stringer("dir", destDocDir).Log()
err = destDocDir.MakeAllDirs()
if err != nil {
return "", err
}
}
companyID, err := conn.DocumentCompanyID(ctx, docID)
if err != nil {
return "", err
}
companyIDFile := destDocDir.Join("company.id")
log.Debug("Writing file").Stringer("file", companyIDFile).Log()
err = companyIDFile.WriteAllString(companyID.String())
if err != nil {
return "", err
}
for _, version := range versions {
versionInfo, err := conn.DocumentVersionInfo(ctx, docID, version)
if err != nil {
return "", err
}
versionInfoFile := destDocDir.Join(version.String() + ".json")
log.Debug("Writing file").Stringer("file", versionInfoFile).Log()
err = versionInfoFile.WriteJSON(ctx, versionInfo, " ")
if err != nil {
return "", err
}
versionFileProvider, err := conn.DocumentVersionFileProvider(ctx, docID, version)
if err != nil {
return "", err
}
versionDir := destDocDir.Join(version.String())
log.Debug("Making directory").Stringer("dir", versionDir).Log()
err = versionDir.MakeDir()
if err != nil {
return "", err
}
filenames, err := versionFileProvider.ListFiles(ctx)
if err != nil {
return "", err
}
for _, filename := range filenames {
data, err := versionFileProvider.ReadFile(ctx, filename)
if err != nil {
return "", err
}
file := versionDir.Join(filename)
log.Debug("Writing file").Stringer("file", file).Log()
err = file.WriteAllContext(ctx, data)
if err != nil {
return "", err
}
}
}
return destDocDir, nil
}
// CopyAllCompanyDocumentFiles copies the files of all versions of
// all documents of a company to a backup directory.
//
// The backupDir must exist and a directory structures for the documents
// will be created inside the backupDir and returned as docDirs.
// In case of an error the already backed up documents will be returned as docDirs.
//
// If true is passed for overwrite then existing files will be overwritten
// else an error is reeturned when docDir already exists.
func CopyAllCompanyDocumentFiles(ctx context.Context, conn Conn, companyID uu.ID, backupDir fs.File, overwrite bool) (docDirs []fs.File, err error) {
defer errs.WrapWithFuncParams(&err, ctx, companyID, backupDir, overwrite)
err = conn.EnumCompanyDocumentIDs(ctx, companyID, func(ctx context.Context, docID uu.ID) error {
docDir, err := CopyDocumentFiles(ctx, conn, docID, backupDir, overwrite)
if err != nil {
return err
}
docDirs = append(docDirs, docDir)
return nil
})
return docDirs, err
}