-
-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathworkspace.go
217 lines (182 loc) · 5.4 KB
/
workspace.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
package main
import (
"os"
"path/filepath"
"sync"
"time"
"github.com/ncruces/rethinkraw/internal/config"
"github.com/ncruces/rethinkraw/internal/util"
"github.com/ncruces/rethinkraw/pkg/osutil"
)
// RethinkRAW edits happen in a workspace.
//
// Adobe DNG Converter loads RAW files and editing metadata from disk,
// and saves DNG files with their embed previews to disk as well.
//
// A workspace is a temporary directory created for each opened RAW file
// where all edits and conversions take place.
//
// This temporary directory is located on: "$TMPDIR/RethingRAW/[HASH]/"
// The directory is created when the file is first opened,
// and deleted when the workspace is finally closed.
//
// It can contain several files:
// . orig.EXT - a read-only copy of the original RAW file
// . orig.xmp - a sidecar for orig.EXT
// . temp.dng - a DNG used as the target for all conversions
// . edit.dng - a DNG conversion of the original RAW file used for editing previews
//
// Editing settings are loaded from orig.xmp or orig.EXT (in that order).
// The DNG in edit.dng is downscaled to at most 2560 on the widest side.
// When generating a preview, use edit.dng unless the preview requires full resolution.
// If edit.dng is missing, use orig.EXT, ask for a 2560 preview, and save that to edit.dng.
type workspace struct {
hash string // a hash of the original RAW file path
ext string // the extension of the original RAW file
base string // base directory for the workspace
hasPixels bool // have we extracted pixel data?
hasEdit bool // any recent edits?
}
func openWorkspace(path string) (wk workspace, err error) {
wk.hash = util.HashedID(filepath.Clean(path))
wk.ext = filepath.Ext(path)
wk.base = filepath.Join(config.TempDir, wk.hash) + string(filepath.Separator)
workspaces.open(wk.hash)
defer func() {
if err != nil {
if workspaces.delete(wk.hash) {
os.RemoveAll(wk.base)
}
wk = workspace{}
}
}()
// create directory
err = os.MkdirAll(wk.base, 0700)
if err != nil {
return wk, err
}
// have we edited this file recently (10 min)?
fi, err := os.Stat(wk.base + "edit.dng")
if err == nil && time.Since(fi.ModTime()) < 10*time.Minute {
pi, _ := os.Stat(wk.base + "edit.ppm")
wk.hasPixels = pi != nil && !pi.ModTime().Before(fi.ModTime())
wk.hasEdit = true
return wk, err
}
// was this just copied (1 min)?
fi, err = os.Stat(wk.base + "orig" + wk.ext)
if err == nil && time.Since(fi.ModTime()) < time.Minute {
return wk, err
}
// otherwise, copy the original RAW file to orig.EXT
err = osutil.Lnky(path, wk.base+"orig"+wk.ext)
if err != nil {
return wk, err
}
// and load a sidecar for it
err = loadSidecar(path, wk.base+"orig.xmp")
return wk, err
}
func (wk *workspace) close() {
if lru := workspaces.close(wk.hash); lru != "" {
os.RemoveAll(filepath.Join(config.TempDir, lru))
}
}
// A read-only copy of the original RAW file (full resolution).
func (wk *workspace) orig() string {
return wk.base + "orig" + wk.ext
}
// A DNG used as the target for all conversions.
func (wk *workspace) temp() string {
return wk.base + "temp.dng"
}
// A JPG used as the target for export.
func (wk *workspace) jpeg() string {
return wk.base + "temp.jpg"
}
// A DNG conversion of the original RAW file used for editing previews (downscaled to 2560).
func (wk *workspace) edit() string {
return wk.base + "edit.dng"
}
// A RAW pixel map for edit.dng.
func (wk *workspace) pixels() string {
return wk.base + "edit.ppm"
}
// A sidecar for orig.EXT.
func (wk *workspace) origXMP() string {
return wk.base + "orig.xmp"
}
// HTTP is stateless. There is no notion of a file being opened for editing.
//
// A global manager keeps track of which files are currently being edited,
// and how many tasks are pending for each file.
//
// Once a file has no pending tasks, the workspace is eligible for deletion.
// As an optimization, the 3 LRU workspaces are cached.
var workspaces = workspaceLocker{locks: make(map[string]*workspaceLock)}
const workspaceMaxLRU = 3
type workspaceLocker struct {
sync.Mutex
lru []string
locks map[string]*workspaceLock
}
type workspaceLock struct {
sync.Mutex
n int //
}
// Open and lock a workspace.
func (wl *workspaceLocker) open(hash string) {
wl.Lock()
// create a workspace lock
lk, ok := wl.locks[hash]
if !ok {
lk = &workspaceLock{}
wl.locks[hash] = lk
}
lk.n++ // one more pending task
for i, h := range wl.lru {
if h == hash {
// remove workspace from LRU
wl.lru = append(wl.lru[:i], wl.lru[i+1:]...)
}
}
wl.Unlock()
lk.Lock()
}
// Close and unlock a workspace, but cache it.
// Return a workspace to evict.
func (wl *workspaceLocker) close(hash string) (lru string) {
wl.Lock()
lk := wl.locks[hash]
lk.n-- // one less pending task
// are we the last task?
if lk.n <= 0 {
// evict a workspace from LRU
if len(wl.lru) >= workspaceMaxLRU {
lru, wl.lru = wl.lru[0], wl.lru[1:]
}
// add ourselves to LRU
wl.lru = append(wl.lru, hash)
// delete our lock
delete(wl.locks, hash)
}
lk.Unlock()
wl.Unlock()
return lru // return the evicted workspace
}
// Close and unlock a workspace, but don't cache it.
// Return if safe to delete.
func (wl *workspaceLocker) delete(hash string) (ok bool) {
wl.Lock()
lk := wl.locks[hash]
lk.n-- // one less pending task
// are we the last task?
if lk.n <= 0 {
// delete our lock
delete(wl.locks, hash)
ok = true
}
lk.Unlock()
wl.Unlock()
return ok // were we the last task?
}