summaryrefslogtreecommitdiff
path: root/lib/versioning.cpp
blob: 82696e3a99392dc6fcc90f909331574b66eebccd (plain)
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
#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 it = shortnameVersion.begin();
    auto last = shortnameVersion.end();

    auto nextDigit = [&]() -> bool
    {
        if (it == last || !isDigit(*it))
            return false;
        ++it;
        return true;
    };
    auto nextDigits = [&](size_t count) -> bool
    {
        while (count-- > 0)
            if (!nextDigit())
                return false;
        return true;
    };
    auto nextChar = [&](Zchar c) -> bool
    {
        if (it == last || *it != c)
            return false;
        ++it;
        return true;
    };
    auto nextStringI = [&](const Zstring& str) -> bool //windows: ignore case!
    {
        if (last - it < static_cast<ptrdiff_t>(str.size()) || !EqualFilename()(str, Zstring(&*it, str.size())))
            return false;
        it += 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)) &&
           it == last;
}


namespace
{
/*
- handle not existing source
- create target super directories if missing
*/
template <class Function>
void moveItemToVersioning(const Zstring& fullName, //throw FileError
                          const Zstring& relativeName,
                          const Zstring& versioningDirectory,
                          const Zstring& timestamp,
                          VersioningStyle versioningStyle,
                          Function moveObj) //move source -> target; may throw FileError
{
    assert(!startsWith(relativeName, FILE_NAME_SEPARATOR));
    assert(!endsWith  (relativeName, FILE_NAME_SEPARATOR));

    Zstring targetName;
    switch (versioningStyle)
    {
        case VER_STYLE_REPLACE:
            targetName = appendSeparator(versioningDirectory) + relativeName;
            break;

        case VER_STYLE_ADD_TIMESTAMP:
            //assemble time-stamped version name
            targetName = appendSeparator(versioningDirectory) + relativeName + Zstr(' ') + timestamp + getExtension(relativeName);
            assert(impl::isMatchingVersion(afterLast(relativeName, FILE_NAME_SEPARATOR), afterLast(targetName, FILE_NAME_SEPARATOR))); //paranoid? no!
            break;
    }

    try
    {
        moveObj(fullName, targetName); //throw FileError
    }
    catch (FileError&) //expected to fail if target directory is not yet existing!
    {
        if (!somethingExists(fullName)) //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(targetName, FILE_NAME_SEPARATOR);
        if (!dirExists(targetDir)) //->(minor) file system race condition!
        {
            makeDirectory(targetDir); //throw FileError
            moveObj(fullName, targetName); //throw FileError -> this should work now!
        }
        else
            throw;
    }
}


//move source to target across volumes
//no need to check if: - super-directories of target exist - source exists: done by moveItemToVersioning()
//if target already exists, it is overwritten, even if it is a different type, e.g. a directory!
template <class Function>
void moveObject(const Zstring& sourceFile, //throw FileError
                const Zstring& targetFile,
                Function copyDelete) //throw FileError; fallback if move failed
{
    assert(fileExists(sourceFile) || symlinkExists(sourceFile) || !somethingExists(sourceFile)); //we process files and symlinks only

    auto removeTarget = [&]
    {
        //remove target object
        if (dirExists(targetFile)) //directory or dir-symlink
            removeDirectory(targetFile); //throw FileError; we do not expect targetFile to be a directory in general => no callback required
        else //file or (broken) file-symlink
            removeFile(targetFile); //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&)
    {
        removeTarget(); //throw FileError
        copyDelete();   //
    }
    catch (const ErrorTargetExisting&)
    {
        removeTarget(); //throw FileError
        try
        {
            renameFile(sourceFile, targetFile); //throw FileError, ErrorDifferentVolume, ErrorTargetExisting
        }
        catch (const ErrorDifferentVolume&)
        {
            copyDelete(); //throw FileError
        }
    }
}


void moveFile(const Zstring& sourceFile, const Zstring& targetFile, CallbackCopyFile& callback) //throw FileError
{
    moveObject(sourceFile, //throw FileError
               targetFile,
               [&]
    {
        //create target
        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
{
    moveObject(sourceLink, //throw FileError
               targetLink,
               [&]
    {
        //create target
        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!
    });
}


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 (dirExists(fullName)) //dir symlink
            dirs_.push_back(shortName);
        else //file symlink, broken symlink
            files_.push_back(shortName);
        return LINK_SKIP;
    }

    virtual TraverseCallback* onDir(const Zchar* shortName, const Zstring& fullName)
    {
        dirs_.push_back(shortName);
        return nullptr; //DON'T traverse into subdirs; moveDirectory works recursively!
    }

    virtual HandleError reportDirError (const std::wstring& msg, size_t retryNumber)                         { throw FileError(msg); }
    virtual HandleError reportItemError(const std::wstring& msg, size_t retryNumber, const Zchar* shortName) { throw FileError(msg); }

    std::vector<Zstring>& files_;
    std::vector<Zstring>& dirs_;
};
}


bool FileVersioner::revisionFile(const Zstring& fullName, const Zstring& relativeName, CallbackMoveFile& callback) //throw FileError
{
    struct CallbackMoveFileImpl : public CallbackMoveDir
    {
        CallbackMoveFileImpl(CallbackMoveFile& callback) : callback_(callback) {}
    private:
        virtual void onBeforeFileMove(const Zstring& fileFrom, const Zstring& fileTo) {}
        virtual void onBeforeDirMove (const Zstring& dirFrom,  const Zstring& dirTo ) {}
        virtual void updateStatus(Int64 bytesDelta) { callback_.updateStatus(bytesDelta); }
        CallbackMoveFile& callback_;
    } cb(callback);

    return revisionFileImpl(fullName, relativeName, cb); //throw FileError
}


bool FileVersioner::revisionFileImpl(const Zstring& fullName, const Zstring& relativeName, CallbackMoveDir& callback) //throw FileError
{
    bool moveSuccessful = false;

    moveItemToVersioning(fullName, //throw FileError
                         relativeName,
                         versioningDirectory_,
                         timeStamp_,
                         versioningStyle_,
                         [&](const Zstring& source, const Zstring& target)
    {
        callback.onBeforeFileMove(source, target); //if we're called by revisionDirImpl() we know that "source" exists!
        //when called by revisionFile(), "source" might not exist, however onBeforeFileMove() is not propagated in this case!

        struct CopyCallbackImpl : public CallbackCopyFile
        {
            CopyCallbackImpl(CallbackMoveDir& callback) : callback_(callback) {}
        private:
            virtual void deleteTargetFile(const Zstring& targetFile) { assert(!somethingExists(targetFile)); }
            virtual void updateCopyStatus(Int64 bytesDelta) { callback_.updateStatus(bytesDelta); }
            CallbackMoveDir& callback_;
        } copyCallback(callback);

        moveFile(source, target, copyCallback); //throw FileError
        moveSuccessful = true;
    });
    return moveSuccessful;
}


void FileVersioner::revisionDir(const Zstring& fullName, const Zstring& relativeName, CallbackMoveDir& callback) //throw FileError
{
    //no error situation if directory is not existing! manual deletion relies on it!
    if (!somethingExists(fullName))
        return; //neither directory nor any other object (e.g. broken symlink) with that name existing
    revisionDirImpl(fullName, relativeName, callback); //throw FileError
}


void FileVersioner::revisionDirImpl(const Zstring& fullName, const Zstring& relativeName, CallbackMoveDir& callback) //throw FileError
{
    assert(somethingExists(fullName)); //[!]

    //create target
    if (symlinkExists(fullName)) //on Linux there is just one type of symlink, and since we do revision file symlinks, we should revision dir symlinks as well!
    {
        moveItemToVersioning(fullName, //throw FileError
                             relativeName,
                             versioningDirectory_,
                             timeStamp_,
                             versioningStyle_,
                             [&](const Zstring& source, const Zstring& target)
        {
            callback.onBeforeDirMove(source, target);
            moveDirSymlink(source, target); //throw FileError
        });
    }
    else
    {
        assert(!startsWith(relativeName, FILE_NAME_SEPARATOR));
        assert(endsWith(fullName, relativeName)); //usually, yes, but we might relax this in the future
        const Zstring targetDir = appendSeparator(versioningDirectory_) + relativeName;

        //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;  //
        {
            TraverseFilesOneLevel tol(fileList, dirList); //throw FileError
            traverseFolder(fullName, tol);               //
        }

        const Zstring fullNamePf = appendSeparator(fullName);
        const Zstring relnamePf   = appendSeparator(relativeName);

        //move files
        std::for_each(fileList.begin(), fileList.end(),
                      [&](const Zstring& shortname)
        {
            revisionFileImpl(fullNamePf + shortname, //throw FileError
                             relnamePf + shortname,
                             callback);
        });

        //move items in subdirectories
        std::for_each(dirList.begin(), dirList.end(),
                      [&](const Zstring& shortname)
        {
            revisionDirImpl(fullNamePf + shortname, //throw FileError
                            relnamePf + shortname,
                            callback);
        });

        //delete source
        callback.onBeforeDirMove(fullName, targetDir);
        removeDirectory(fullName); //throw FileError
    }
}


/*
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 reportDirError (const std::wstring& msg)                         { throw FileError(msg); }
    virtual HandleError reportItemError(const std::wstring& msg, const Zchar* shortName) { 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 it = dirBuffer.find(dirname);
        if (it != dirBuffer.end())
            return it->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 ZEN_WIN //if it's a directory symlink:
                if (symlinkExists(fullnameVer) && dirExists(fullnameVer))
                    removeDirectory(fullnameVer); //throw FileError
                else
#endif
                    throw;
            }
        });
    });
}
*/
bgstack15