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
|
#include "versioning.h"
#include <map>
#include <zen/file_handling.h>
#include <zen/file_traverser.h>
#include <zen/string_tools.h>
using namespace zen;
namespace
{
Zstring getExtension(const Zstring& relativeName) //including "." if extension is existing, returns empty string otherwise
{
auto iterSep = find_last(relativeName.begin(), relativeName.end(), FILE_NAME_SEPARATOR);
auto iterName = iterSep != relativeName.end() ? iterSep + 1 : relativeName.begin(); //find beginning of short name
auto iterDot = find_last(iterName, relativeName.end(), Zstr('.')); //equal to relativeName.end() if file has no extension!!
return Zstring(&*iterDot, relativeName.end() - iterDot);
};
}
bool impl::isMatchingVersion(const Zstring& shortname, const Zstring& shortnameVersion) //e.g. ("Sample.txt", "Sample.txt 2012-05-15 131513.txt")
{
auto iter = shortnameVersion.begin();
auto last = shortnameVersion.end();
auto nextDigit = [&]() -> bool
{
if (iter == last || !isDigit(*iter))
return false;
++iter;
return true;
};
auto nextDigits = [&](size_t count) -> bool
{
while (count-- > 0)
if (!nextDigit())
return false;
return true;
};
auto nextChar = [&](Zchar c) -> bool
{
if (iter == last || *iter != c)
return false;
++iter;
return true;
};
auto nextStringI = [&](const Zstring& str) -> bool //windows: ignore case!
{
if (last - iter < static_cast<ptrdiff_t>(str.size()) || !EqualFilename()(str, Zstring(&*iter, str.size())))
return false;
iter += str.size();
return true;
};
return nextStringI(shortname) && //versioned file starts with original name
nextChar(Zstr(' ')) && //validate timestamp: e.g. "2012-05-15 131513"; Regex: \d{4}-\d{2}-\d{2} \d{6}
nextDigits(4) && //YYYY
nextChar(Zstr('-')) && //
nextDigits(2) && //MM
nextChar(Zstr('-')) && //
nextDigits(2) && //DD
nextChar(Zstr(' ')) && //
nextDigits(6) && //HHMMSS
nextStringI(getExtension(shortname)) &&
iter == last;
}
namespace
{
template <class Function>
void moveItemToVersioning(const Zstring& sourceObj, //throw FileError
const Zstring& relativeName,
const Zstring& versioningDirectory,
const Zstring& timestamp,
Function moveObj) //move source -> target; allowed to throw FileError
{
assert(!startsWith(relativeName, FILE_NAME_SEPARATOR));
assert(endsWith(sourceObj, relativeName)); //usually, yes, but we might relax this in the future
//assemble time-stamped version name
const Zstring targetObj = appendSeparator(versioningDirectory) + relativeName + Zstr(' ') + timestamp + getExtension(relativeName);
assert(impl::isMatchingVersion(afterLast(relativeName, FILE_NAME_SEPARATOR), afterLast(targetObj, FILE_NAME_SEPARATOR))); //paranoid? no!
try
{
moveObj(sourceObj, targetObj); //throw FileError
}
catch (FileError&) //expected to fail if target directory is not yet existing!
{
if (!somethingExists(sourceObj)) //no source at all is not an error (however a directory as source when a file is expected, *is* an error!)
return; //object *not* processed
//create intermediate directories if missing
const Zstring targetDir = beforeLast(targetObj, FILE_NAME_SEPARATOR);
if (!dirExists(targetDir)) //->(minor) file system race condition!
{
makeDirectory(targetDir); //throw FileError
moveObj(sourceObj, targetObj); //throw FileError -> this should work now!
}
else
throw;
}
}
//move source to target across volumes; prerequisite: all super-directories of target exist
//if target already contains some files/dirs they are seen as remnants of a previous incomplete move
void moveFile(const Zstring& sourceFile, const Zstring& targetFile, CallbackCopyFile& callback) //throw FileError
{
//first try to move directly without copying
try
{
renameFile(sourceFile, targetFile); //throw FileError, ErrorDifferentVolume, ErrorTargetExisting
return; //great, we get away cheaply!
}
//if moving failed treat as error (except when it tried to move to a different volume: in this case we will copy the file)
catch (const ErrorDifferentVolume&) {}
catch (const ErrorTargetExisting&) {}
//create target
if (!fileExists(targetFile)) //check even if ErrorTargetExisting: me may have clashed with another item type of the same name!!!
{
//file is on a different volume: let's copy it
if (symlinkExists(sourceFile))
copySymlink(sourceFile, targetFile, false); //throw FileError; don't copy filesystem permissions
else
copyFile(sourceFile, targetFile, false, true, &callback); //throw FileError - permissions "false", transactional copy "true"
}
//delete source
removeFile(sourceFile); //throw FileError; newly copied file is NOT deleted if exception is thrown here!
}
void moveDirSymlink(const Zstring& sourceLink, const Zstring& targetLink) //throw FileError
{
//first try to move directly without copying
try
{
renameFile(sourceLink, targetLink); //throw FileError, ErrorDifferentVolume, ErrorTargetExisting
return; //great, we get away cheaply!
}
//if moving failed treat as error (except when it tried to move to a different volume: in this case we will copy the file)
catch (const ErrorDifferentVolume&) {}
catch (const ErrorTargetExisting&) {}
//create target
if (!symlinkExists(targetLink)) //check even if ErrorTargetExisting: me may have clashed with another item type of the same name!!!
{
//link is on a different volume: let's copy it
copySymlink(sourceLink, targetLink, false); //throw FileError; don't copy filesystem permissions
}
//delete source
removeDirectory(sourceLink); //throw FileError; newly copied link is NOT deleted if exception is thrown here!
}
struct CopyCallbackImpl : public CallbackCopyFile
{
CopyCallbackImpl(CallbackMoveFile& callback) : callback_(callback) {}
private:
virtual void deleteTargetFile(const Zstring& targetFile) { assert(!somethingExists(targetFile)); }
virtual void updateCopyStatus(Int64 bytesDelta) { callback_.updateStatus(bytesDelta); }
CallbackMoveFile& callback_;
};
class TraverseFilesOneLevel : public TraverseCallback
{
public:
TraverseFilesOneLevel(std::vector<Zstring>& files, std::vector<Zstring>& dirs) : files_(files), dirs_(dirs) {}
private:
virtual void onFile(const Zchar* shortName, const Zstring& fullName, const FileInfo& details)
{
files_.push_back(shortName);
}
virtual HandleLink onSymlink(const Zchar* shortName, const Zstring& fullName, const SymlinkInfo& details)
{
if (details.dirLink)
dirs_.push_back(shortName);
else
files_.push_back(shortName);
return LINK_SKIP;
}
virtual std::shared_ptr<TraverseCallback> onDir(const Zchar* shortName, const Zstring& fullName)
{
dirs_.push_back(shortName);
return nullptr; //DON'T traverse into subdirs; moveDirectory works recursively!
}
virtual HandleError onError(const std::wstring& msg) { throw FileError(msg); }
std::vector<Zstring>& files_;
std::vector<Zstring>& dirs_;
};
struct RemoveCallbackImpl : public CallbackRemoveDir
{
RemoveCallbackImpl(CallbackMoveFile& callback) : callback_(callback) {}
private:
virtual void notifyFileDeletion(const Zstring& filename) { callback_.updateStatus(0); }
virtual void notifyDirDeletion (const Zstring& dirname ) { callback_.updateStatus(0); }
CallbackMoveFile& callback_;
};
}
void FileVersioner::revisionFile(const Zstring& sourceFile, const Zstring& relativeName, CallbackMoveFile& callback) //throw FileError
{
moveItemToVersioning(sourceFile, //throw FileError
relativeName,
versioningDirectory_,
timeStamp_,
[&](const Zstring& source, const Zstring& target)
{
callback.onBeforeFileMove(source, target);
CopyCallbackImpl copyCallback(callback);
moveFile(source, target, copyCallback); //throw FileError
callback.objectProcessed();
});
fileRelnames.push_back(relativeName);
}
void FileVersioner::revisionDir(const Zstring& sourceDir, const Zstring& relativeName, CallbackMoveFile& callback) //throw FileError
{
//note: we cannot support "throw exception if target already exists": If we did, we would have to do a full cleanup
//removing all newly created directories in case of an exception so that subsequent tries would not fail with "target already existing".
//However an exception may also happen during final deletion of source folder, in which case cleanup effectively leads to data loss!
//create target
if (symlinkExists(sourceDir)) //on Linux there is just one type of symlinks, and since we do revision file symlinks, we should revision dir symlinks as well!
{
moveItemToVersioning(sourceDir, //throw FileError
relativeName,
versioningDirectory_,
timeStamp_,
[&](const Zstring& source, const Zstring& target)
{
callback.onBeforeDirMove(source, target);
moveDirSymlink(source, target); //throw FileError
callback.objectProcessed();
});
fileRelnames.push_back(relativeName);
}
else
{
assert(!startsWith(relativeName, FILE_NAME_SEPARATOR));
assert(endsWith(sourceDir, relativeName));
const Zstring targetDir = appendSeparator(versioningDirectory_) + relativeName;
callback.onBeforeDirMove(sourceDir, targetDir);
//makeDirectory(targetDir); //FileError -> create only when needed in moveFileToVersioning(); avoids empty directories
//traverse source directory one level
std::vector<Zstring> fileList; //list of *short* names
std::vector<Zstring> dirList; //
try
{
TraverseFilesOneLevel tol(fileList, dirList); //throw FileError
traverseFolder(sourceDir, tol); //
}
catch (FileError&)
{
if (!somethingExists(sourceDir)) //no source at all is not an error (however a file as source when a directory is expected, *is* an error!)
return; //object *not* processed
throw;
}
const Zstring sourceDirPf = appendSeparator(sourceDir);
const Zstring relnamePf = appendSeparator(relativeName);
//move files
std::for_each(fileList.begin(), fileList.end(),
[&](const Zstring& shortname)
{
revisionFile(sourceDirPf + shortname, //throw FileError
relnamePf + shortname,
callback);
});
//move directories
std::for_each(dirList.begin(), dirList.end(),
[&](const Zstring& shortname)
{
revisionDir(sourceDirPf + shortname, //throw FileError
relnamePf + shortname,
callback);
});
//delete source
RemoveCallbackImpl removeCallback(callback);
removeDirectory(sourceDir, &removeCallback); //throw FileError
callback.objectProcessed();
}
}
namespace
{
class TraverseVersionsOneLevel : public TraverseCallback
{
public:
TraverseVersionsOneLevel(std::vector<Zstring>& files, std::function<void()> updateUI) : files_(files), updateUI_(updateUI) {}
private:
virtual void onFile(const Zchar* shortName, const Zstring& fullName, const FileInfo& details) { files_.push_back(shortName); updateUI_(); }
virtual HandleLink onSymlink(const Zchar* shortName, const Zstring& fullName, const SymlinkInfo& details) { files_.push_back(shortName); updateUI_(); return LINK_SKIP; }
virtual std::shared_ptr<TraverseCallback> onDir(const Zchar* shortName, const Zstring& fullName) { updateUI_(); return nullptr; } //DON'T traverse into subdirs
virtual HandleError onError(const std::wstring& msg) { throw FileError(msg); }
std::vector<Zstring>& files_;
std::function<void()> updateUI_;
};
}
void FileVersioner::limitVersions(std::function<void()> updateUI) //throw FileError
{
if (versionCountLimit_ < 0) //no limit!
return;
//buffer map "directory |-> list of immediate child file and symlink short names"
std::map<Zstring, std::vector<Zstring>, LessFilename> dirBuffer;
auto getVersionsBuffered = [&](const Zstring& dirname) -> const std::vector<Zstring>&
{
auto iter = dirBuffer.find(dirname);
if (iter != dirBuffer.end())
return iter->second;
std::vector<Zstring> fileShortNames;
TraverseVersionsOneLevel tol(fileShortNames, updateUI); //throw FileError
traverseFolder(dirname, tol);
auto& newEntry = dirBuffer[dirname]; //transactional behavior!!!
newEntry.swap(fileShortNames); //-> until C++11 emplace is available
return newEntry;
};
std::for_each(fileRelnames.begin(), fileRelnames.end(),
[&](const Zstring& relativeName) //e.g. "subdir\Sample.txt"
{
const Zstring fullname = appendSeparator(versioningDirectory_) + relativeName; //e.g. "D:\Revisions\subdir\Sample.txt"
const Zstring parentDir = beforeLast(fullname, FILE_NAME_SEPARATOR); //e.g. "D:\Revisions\subdir"
const Zstring shortname = afterLast(relativeName, FILE_NAME_SEPARATOR); //e.g. "Sample.txt"; returns the whole string if seperator not found
const std::vector<Zstring>& allVersions = getVersionsBuffered(parentDir);
//filter out only those versions that match the given relative name
std::vector<Zstring> matches; //e.g. "Sample.txt 2012-05-15 131513.txt"
std::copy_if(allVersions.begin(), allVersions.end(), std::back_inserter(matches),
[&](const Zstring& shortnameVer) { return impl::isMatchingVersion(shortname, shortnameVer); });
//take advantage of version naming convention to find oldest versions
if (matches.size() <= static_cast<size_t>(versionCountLimit_))
return;
std::nth_element(matches.begin(), matches.end() - versionCountLimit_, matches.end(), LessFilename()); //windows: ignore case!
//delete obsolete versions
std::for_each(matches.begin(), matches.end() - versionCountLimit_,
[&](const Zstring& shortnameVer)
{
updateUI();
const Zstring fullnameVer = parentDir + FILE_NAME_SEPARATOR + shortnameVer;
try
{
removeFile(fullnameVer); //throw FileError
}
catch (FileError&)
{
#ifdef FFS_WIN //if it's a directory symlink:
if (symlinkExists(fullnameVer) && dirExists(fullnameVer))
removeDirectory(fullnameVer); //throw FileError
else
#endif
throw;
}
});
});
}
|