-
-
Notifications
You must be signed in to change notification settings - Fork 18
/
mmv.go
265 lines (239 loc) · 6.21 KB
/
mmv.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
// Package mmv provides a method to rename multiple files.
package mmv
import (
"crypto/rand"
"encoding/base64"
"os"
"path/filepath"
"strings"
)
// Rename multiple files.
func Rename(files map[string]string) error {
rs, err := buildRenames(files)
if err != nil {
return err
}
for i, r := range rs {
if err := doRename(r.src, r.dst); err != nil {
// undo on error not to leave the temporary files
// this does not undo directory creation
for i--; i >= 0; i-- {
if r = rs[i]; os.Rename(r.dst, r.src) != nil {
// something wrong happens so give up not to overwrite files
break
}
}
return err
}
}
return nil
}
// rename with creating the destination directory
func doRename(src, dst string) (err error) {
// first of all, try renaming the file, which will succeed in most cases
if err = os.Rename(src, dst); err != nil && os.IsNotExist(err) {
// check the source file existence to exit without creating the destination
// directory when the both source file and destination directory do not exist
if _, err := os.Stat(src); err != nil {
return err
}
// create the destination directory
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}
// try renaming again
return os.Rename(src, dst)
}
return
}
type rename struct {
src, dst string
}
type emptyPathError struct{}
func (err *emptyPathError) Error() string {
return "empty path error"
}
type sameSourceError struct {
path string
}
func (err *sameSourceError) Error() string {
return "duplicate source: " + err.path
}
type sameDestinationError struct {
path string
}
func (err *sameDestinationError) Error() string {
return "duplicate destination: " + err.path
}
type invalidRenameError struct {
src, dst string
}
func (err *invalidRenameError) Error() string {
return "invalid rename: " + err.src + ", " + err.dst
}
type temporaryPathError struct {
dir string
}
func (err *temporaryPathError) Error() string {
return "failed to create a temporary path: " + err.dir
}
func buildRenames(files map[string]string) ([]rename, error) {
revs := make(map[string]string, len(files)) // reverse of files
// list the current rename sources
srcs := make([]string, 0, len(files))
for src := range files {
srcs = append(srcs, src)
}
// clean the paths and check duplication
for _, src := range srcs {
dst := files[src]
if src == "" || dst == "" {
return nil, &emptyPathError{}
}
if d := filepath.Clean(src); d != src {
delete(files, src)
src = d
if _, ok := files[src]; ok {
return nil, &sameSourceError{src}
}
files[src] = dst
}
if d := filepath.Clean(dst); d != dst {
dst = d
files[src] = dst
}
if _, ok := revs[dst]; ok {
return nil, &sameDestinationError{dst}
}
if k, l := len(src), len(dst); k > l && src[l] == filepath.Separator && src[:l] == dst ||
k < l && dst[k] == filepath.Separator && dst[:k] == src {
return nil, &invalidRenameError{src, dst}
}
revs[dst] = src
}
// group paths by directory depth
srcdepths := make([][]string, 1)
dstdepths := make([][]string, 1)
for src, dst := range files {
// group source paths by directory depth
i := strings.Count(src, string(filepath.Separator))
if len(srcdepths) <= i {
xs := make([][]string, i*2)
copy(xs, srcdepths)
srcdepths = xs
}
srcdepths[i] = append(srcdepths[i], src)
// group destination paths by directory depth
i = strings.Count(dst, string(filepath.Separator))
if len(dstdepths) <= i {
xs := make([][]string, i*2)
copy(xs, dstdepths)
dstdepths = xs
}
dstdepths[i] = append(dstdepths[i], dst)
}
// result renames
count := len(files)
rs := make([]rename, 0, 2*count)
// check if any parent directory will be moved
for i := len(srcdepths) - 1; i >= 0; i-- {
L:
for _, src := range srcdepths[i] {
for j := 0; j < i; j++ {
for _, s := range srcdepths[j] {
if k := len(s); len(src) > k && src[k] == filepath.Separator && src[:k] == s {
if d := files[s]; s != d {
if dst, l := files[src], len(d); i == j+1 && len(dst) > l && dst[:l] == d && dst[l:] == src[k:] {
// skip moving a file when it moves along with the closest parent directory
delete(files, src)
delete(revs, dst)
} else {
// move to a temporary path before any parent directory is moved
tmp, err := temporaryPath(filepath.Dir(s))
if err != nil {
return nil, err
}
rs = append(rs, rename{src, tmp})
files[tmp] = files[dst]
delete(files, src)
revs[dst] = tmp
}
continue L
}
}
}
}
// remove if source path is equal to destination path
if dst := files[src]; src == dst {
delete(files, src)
delete(revs, dst)
}
}
}
// list renames in increasing destination directory depth order
i, vs := 0, make(map[string]int, count)
for _, dsts := range dstdepths {
for _, dst := range dsts {
if vs[dst] > 0 {
continue
}
i++ // connected component identifier
// mark the nodes in the connected component and check cycle
var cycle bool
for {
vs[dst] = i
if x, ok := files[dst]; ok {
dst = x
if vs[x] > 0 {
cycle = vs[x] == i
break
}
} else {
break
}
}
// if there is a cycle, rename to a temporary file
var tmp string
if cycle {
var err error
tmp, err = temporaryPath(filepath.Dir(dst))
if err != nil {
return nil, err
}
rs = append(rs, rename{dst, tmp})
vs[dst]--
}
// rename from the leaf node
for {
if src, ok := revs[dst]; ok && (!cycle || vs[src] == i) {
rs = append(rs, rename{src, dst})
if !cycle {
vs[dst] = i
}
dst = src
} else {
break
}
}
// if there is a cycle, rename the temporary file
if cycle {
rs = append(rs, rename{tmp, dst})
}
}
}
return rs, nil
}
// create a temporary path where there is no file currently
func temporaryPath(dir string) (string, error) {
bs := make([]byte, 16)
for i := 0; i < 256; i++ {
if _, err := rand.Read(bs); err != nil {
return "", err
}
path := filepath.Join(dir, base64.RawURLEncoding.EncodeToString(bs))
if _, err := os.Stat(path); err != nil && os.IsNotExist(err) {
return path, nil
}
}
return "", &temporaryPathError{dir}
}