Folder.cpp 13.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
/*****************************************************************************
 * Media Library
 *****************************************************************************
 * Copyright (C) 2015 Hugo Beauzée-Luyssen, Videolabs
 *
 * Authors: Hugo Beauzée-Luyssen<hugo@beauzee.fr>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

23 24 25 26
#if HAVE_CONFIG_H
# include "config.h"
#endif

27
#include "File.h"
28
#include "Folder.h"
29
#include "Device.h"
30
#include "Media.h"
31

32
#include "database/SqliteTools.h"
33 34
#include "medialibrary/filesystem/IDirectory.h"
#include "medialibrary/filesystem/IDevice.h"
35
#include "medialibrary/filesystem/IFileSystemFactory.h"
36 37 38
#include "utils/Filename.h"

#include <unordered_map>
39

40 41 42
namespace medialibrary
{

43 44 45
namespace policy
{
    const std::string FolderTable::Name = "Folder";
46
    const std::string FolderTable::PrimaryKeyColumn = "id_folder";
47
    int64_t Folder::* const FolderTable::PrimaryKey = &Folder::m_id;
48 49
}

50 51
Folder::Folder( MediaLibraryPtr ml, sqlite::Row& row )
    : m_ml( ml )
52
{
53
    bool dummy;
54 55 56
    row >> m_id
        >> m_path
        >> m_parent
57
        >> m_isBlacklisted
58
        >> m_deviceId
59
        >> dummy
60
        >> m_isRemovable;
61 62
}

63
Folder::Folder(MediaLibraryPtr ml, const std::string& path, int64_t parent, int64_t deviceId, bool isRemovable )
64 65
    : m_ml( ml )
    , m_id( 0 )
66
    , m_path( path )
67
    , m_parent( parent )
68
    , m_isBlacklisted( false )
69
    , m_deviceId( deviceId )
70
    , m_isRemovable( isRemovable )
71 72 73
{
}

74
void Folder::createTable( sqlite::Connection* connection)
75
{
76 77 78
    std::string req = "CREATE TABLE IF NOT EXISTS " + policy::FolderTable::Name +
            "("
            "id_folder INTEGER PRIMARY KEY AUTOINCREMENT,"
79
            "path TEXT,"
80
            "parent_id UNSIGNED INTEGER,"
81
            "is_blacklisted BOOLEAN NOT NULL DEFAULT 0,"
82
            "device_id UNSIGNED INTEGER,"
83
            "is_present BOOLEAN NOT NULL DEFAULT 1,"
84
            "is_removable BOOLEAN NOT NULL,"
85
            "FOREIGN KEY (parent_id) REFERENCES " + policy::FolderTable::Name +
86 87
            "(id_folder) ON DELETE CASCADE,"
            "FOREIGN KEY (device_id) REFERENCES " + policy::DeviceTable::Name +
88 89
            "(id_device) ON DELETE CASCADE,"
            "UNIQUE(path, device_id) ON CONFLICT FAIL"
90
            ")";
91 92 93 94 95 96
    std::string exclEntryReq = "CREATE TABLE IF NOT EXISTS ExcludedEntryFolder("
                               "folder_id UNSIGNED INTEGER NOT NULL,"
                               "FOREIGN KEY (folder_id) REFERENCES " + policy::FolderTable::Name +
                               "(id_folder) ON DELETE CASCADE,"
                               "UNIQUE(folder_id) ON CONFLICT FAIL"
                               ")";
97 98 99 100 101 102 103

    sqlite::Tools::executeRequest( connection, req );
    sqlite::Tools::executeRequest( connection, exclEntryReq );
}

void Folder::createTriggers( sqlite::Connection* connection )
{
104 105
    std::string triggerReq = "CREATE TRIGGER IF NOT EXISTS is_device_present AFTER UPDATE OF is_present ON "
            + policy::DeviceTable::Name +
106
            " WHEN old.is_present != new.is_present"
107 108 109
            " BEGIN"
            " UPDATE " + policy::FolderTable::Name + " SET is_present = new.is_present WHERE device_id = new.id_device;"
            " END";
110 111 112 113
    std::string deviceIndexReq = "CREATE INDEX IF NOT EXISTS folder_device_id_idx ON " +
            policy::FolderTable::Name + " (device_id)";
    std::string parentFolderIndexReq = "CREATE INDEX IF NOT EXISTS parent_folder_id_idx ON " +
            policy::FolderTable::Name + " (parent_id)";
114

115 116 117
    sqlite::Tools::executeRequest( connection, triggerReq );
    sqlite::Tools::executeRequest( connection, deviceIndexReq );
    sqlite::Tools::executeRequest( connection, parentFolderIndexReq );
118 119
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
120
std::shared_ptr<Folder> Folder::create( MediaLibraryPtr ml, const std::string& mrl,
121
                                        int64_t parentId, Device& device, fs::IDevice& deviceFs )
122
{
123 124
    std::string path;
    if ( device.isRemovable() == true )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
125
        path = utils::file::removePath( mrl, deviceFs.mountpoint() );
126
    else
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
127
        path = mrl;
128
    auto self = std::make_shared<Folder>( ml, path, parentId, device.id(), device.isRemovable() );
129
    static const std::string req = "INSERT INTO " + policy::FolderTable::Name +
130
            "(path, parent_id, device_id, is_removable) VALUES(?, ?, ?, ?)";
131
    if ( insert( ml, self, req, path, sqlite::ForeignKey( parentId ), device.id(), device.isRemovable() ) == false )
132
        return nullptr;
133 134 135 136 137
    if ( device.isRemovable() == true )
    {
        self->m_deviceMountpoint = deviceFs.mountpoint();
        self->m_fullPath = self->m_deviceMountpoint.get() + path;
    }
138 139 140
    return self;
}

141 142 143 144 145 146
void Folder::excludeEntryFolder( MediaLibraryPtr ml, int64_t folderId )
{
    std::string req = "INSERT INTO ExcludedEntryFolder(folder_id) VALUES(?)";
    sqlite::Tools::executeRequest( ml->getConn(), req, folderId );
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
147
bool Folder::blacklist( MediaLibraryPtr ml, const std::string& mrl )
148
{
149
    // Ensure we delete the existing folder if any & blacklist the folder in an "atomic" way
150 151
    return sqlite::Tools::withRetries( 3, [ml, &mrl]() {
        auto t = ml->getConn()->newTransaction();
152

153 154 155 156 157 158 159 160 161 162 163 164
        auto f = fromMrl( ml, mrl, BannedType::Any );
        if ( f != nullptr )
        {
            // No need to blacklist a folder twice
            if ( f->m_isBlacklisted == true )
                return true;
            // Let the foreign key destroy everything beneath this folder
            destroy( ml, f->id() );
        }
        auto fsFactory = ml->fsFactoryForMrl( mrl );
        if ( fsFactory == nullptr )
            return false;
165 166 167 168 169 170 171 172 173 174
        std::shared_ptr<fs::IDirectory> folderFs;
        try
        {
            folderFs = fsFactory->createDirectory( mrl );
        }
        catch ( std::system_error& ex )
        {
            LOG_ERROR( "Failed to instantiate a directory to ban folder: ", ex.what() );
            return false;
        }
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
        auto deviceFs = folderFs->device();
        if ( deviceFs == nullptr )
        {
            LOG_ERROR( "Can't find device associated with mrl ", mrl );
            return false;
        }
        auto device = Device::fromUuid( ml, deviceFs->uuid() );
        if ( device == nullptr )
            device = Device::create( ml, deviceFs->uuid(), utils::file::scheme( mrl ), deviceFs->isRemovable() );
        std::string path;
        if ( deviceFs->isRemovable() == true )
            path = utils::file::removePath( mrl, deviceFs->mountpoint() );
        else
            path = mrl;
        static const std::string req = "INSERT INTO " + policy::FolderTable::Name +
                "(path, parent_id, is_blacklisted, device_id, is_removable) VALUES(?, ?, ?, ?, ?)";
        auto res = sqlite::Tools::executeInsert( ml->getConn(), req, path, nullptr, true, device->id(), deviceFs->isRemovable() ) != 0;
        t->commit();
        return res;
    });
195 196
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
197
std::shared_ptr<Folder> Folder::fromMrl( MediaLibraryPtr ml, const std::string& mrl )
198
{
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
199
    return fromMrl( ml, mrl, BannedType::No );
200 201
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
202
std::shared_ptr<Folder> Folder::blacklistedFolder( MediaLibraryPtr ml, const std::string& mrl )
203
{
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
204
    return fromMrl( ml, mrl, BannedType::Yes );
205 206
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
207
std::shared_ptr<Folder> Folder::fromMrl( MediaLibraryPtr ml, const std::string& mrl, BannedType bannedType )
208
{
209 210
    if ( mrl.empty() == true )
        return nullptr;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
211
    auto fsFactory = ml->fsFactoryForMrl( mrl );
212 213
    if ( fsFactory == nullptr )
        return nullptr;
214 215 216 217 218 219 220 221 222 223 224
    std::shared_ptr<fs::IDirectory> folderFs;
    try
    {
        folderFs = fsFactory->createDirectory( mrl );
    }
    catch ( const std::system_error& ex )
    {
        LOG_ERROR( "Failed to instanciate a folder for mrl: ", mrl, ": ", ex.what() );
        return nullptr;
    }

225 226 227
    auto deviceFs = folderFs->device();
    if ( deviceFs == nullptr )
    {
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
228
        LOG_ERROR( "Failed to get device containing an existing folder: ", folderFs->mrl() );
229 230
        return nullptr;
    }
231 232
    if ( deviceFs->isRemovable() == false )
    {
233 234
        std::string req = "SELECT * FROM " + policy::FolderTable::Name + " WHERE path = ? AND is_removable = 0";
        if ( bannedType == BannedType::Any )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
235
            return fetch( ml, req, folderFs->mrl() );
236
        req += " AND is_blacklisted = ?";
237
        return fetch( ml, req, folderFs->mrl(), bannedType == BannedType::Yes ? true : false );
238 239
    }

240
    auto device = Device::fromUuid( ml, deviceFs->uuid() );
241 242 243
    // We are trying to find a folder. If we don't know the device it's on, we don't know the folder.
    if ( device == nullptr )
        return nullptr;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
244
    auto path = utils::file::removePath( folderFs->mrl(), deviceFs->mountpoint() );
245 246 247 248 249 250 251 252 253 254 255
    std::string req = "SELECT * FROM " + policy::FolderTable::Name + " WHERE path = ? AND device_id = ?";
    std::shared_ptr<Folder> folder;
    if ( bannedType == BannedType::Any )
    {
        folder = fetch( ml, req, path, device->id() );
    }
    else
    {
        req += " AND is_blacklisted = ?";
        folder = fetch( ml, req, path, device->id(), bannedType == BannedType::Yes ? true : false );
    }
256 257 258
    if ( folder == nullptr )
        return nullptr;
    folder->m_deviceMountpoint = deviceFs->mountpoint();
259
    folder->m_fullPath = folder->m_deviceMountpoint.get() + path;
260
    return folder;
261 262
}

263
int64_t Folder::id() const
264 265 266 267
{
    return m_id;
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
268
const std::string& Folder::mrl() const
269
{
270 271 272
    if ( m_isRemovable == false )
        return m_path;

273 274 275 276
    auto lock = m_deviceMountpoint.lock();
    if ( m_deviceMountpoint.isCached() == true )
        return m_fullPath;

277 278 279 280 281 282 283 284 285 286 287
    // We can't compute the full path of a folder if it's removable and the device isn't present.
    // When there's no device, we don't know the mountpoint, therefor we don't know the full path
    // Calling isPresent will ensure we have the device representation cached locally
    if ( isPresent() == false )
    {
        assert( !"Device isn't present" );
        m_fullPath = "";
        return m_fullPath;
    }

    auto fsFactory = m_ml->fsFactoryForMrl( m_device.get()->scheme() );
288
    assert( fsFactory != nullptr );
289
    auto deviceFs = fsFactory->createDevice( m_device.get()->uuid() );
290 291 292 293 294 295 296 297
    // In case the device lister hasn't been updated accordingly, we might think
    // a device still is present while it's not.
    if( deviceFs == nullptr )
    {
        assert( !"File system Device representation couldn't be found" );
        m_fullPath = "";
        return m_fullPath;
    }
298 299
    m_deviceMountpoint = deviceFs->mountpoint();
    m_fullPath = m_deviceMountpoint.get() + m_path;
300
    return m_fullPath;
301 302
}

303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
const std::string& Folder::rawMrl() const
{
    return m_path;
}

void Folder::setMrl( std::string mrl )
{
    if ( m_path == mrl )
        return;
    static const std::string req = "UPDATE " + policy::FolderTable::Name + " SET "
            "path = ? WHERE id_folder = ?";
    if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req, mrl, m_id ) == false )
        return;
    // We shouldn't use this if any full path/mrl has been cached.
    // This is meant for migration only, so there is no need to have cached this
    // information so far.
    assert( m_isRemovable == false || m_fullPath.empty() == true );
    m_path = std::move( mrl );
}

323
std::vector<std::shared_ptr<File>> Folder::files()
324
{
325
    static const std::string req = "SELECT * FROM " + policy::FileTable::Name +
326
        " WHERE folder_id = ?";
327
    return File::fetchAll<File>( m_ml, req, m_id );
328
}
329

330
std::vector<std::shared_ptr<Folder>> Folder::folders()
331
{
332
    static const std::string req = "SELECT * FROM " + policy::FolderTable::Name
333
            + " WHERE parent_id = ? AND is_blacklisted = 0 AND is_present != 0";
334
    return DatabaseHelpers::fetchAll<Folder>( m_ml, req, m_id );
335 336
}

337
std::shared_ptr<Folder> Folder::parent()
338
{
339
    return fetch( m_ml, m_parent );
340
}
341

342
int64_t Folder::deviceId() const
343
{
344
    return m_deviceId;
345
}
346 347 348

bool Folder::isPresent() const
{
349 350 351
    auto deviceLock = m_device.lock();
    if ( m_device.isCached() == false )
        m_device = Device::fetch( m_ml, m_deviceId );
352 353
    // There must be a device containing the folder, since we never create a folder
    // without a device
354
    assert( m_device.get() != nullptr );
355 356 357
    // However, handle potential sporadic errors gracefully
    if( m_device.get() == nullptr )
        return false;
358
    return m_device.get()->isPresent();
359
}
360

361 362 363 364 365
bool Folder::isBanned() const
{
    return m_isBlacklisted;
}

366 367 368 369 370
bool Folder::isRootFolder() const
{
    return m_parent == 0;
}

371
std::vector<std::shared_ptr<Folder>> Folder::fetchRootFolders( MediaLibraryPtr ml )
372
{
373 374 375 376
    static const std::string req = "SELECT * FROM " + policy::FolderTable::Name +
            " LEFT JOIN ExcludedEntryFolder"
            " ON " + policy::FolderTable::Name + ".id_folder = ExcludedEntryFolder.folder_id"
            " WHERE ExcludedEntryFolder.folder_id IS NULL AND"
377
            " parent_id IS NULL AND is_blacklisted = 0 AND is_present != 0";
378
    return DatabaseHelpers::fetchAll<Folder>( ml, req );
379
}
380 381

}