Folder.cpp 12.1 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
#include "filesystem/IDirectory.h"
34 35 36 37
#include "filesystem/IDevice.h"
#include "utils/Filename.h"

#include <unordered_map>
38

39 40 41
namespace medialibrary
{

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

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

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

73
void Folder::createTable( sqlite::Connection* connection)
74
{
75 76 77
    std::string req = "CREATE TABLE IF NOT EXISTS " + policy::FolderTable::Name +
            "("
            "id_folder INTEGER PRIMARY KEY AUTOINCREMENT,"
78
            "path TEXT,"
79
            "parent_id UNSIGNED INTEGER,"
80
            "is_blacklisted BOOLEAN NOT NULL DEFAULT 0,"
81
            "device_id UNSIGNED INTEGER,"
82
            "is_present BOOLEAN NOT NULL DEFAULT 1,"
83
            "is_removable BOOLEAN NOT NULL,"
84
            "FOREIGN KEY (parent_id) REFERENCES " + policy::FolderTable::Name +
85 86
            "(id_folder) ON DELETE CASCADE,"
            "FOREIGN KEY (device_id) REFERENCES " + policy::DeviceTable::Name +
87 88
            "(id_device) ON DELETE CASCADE,"
            "UNIQUE(path, device_id) ON CONFLICT FAIL"
89
            ")";
90 91 92 93 94 95
    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"
                               ")";
96 97 98 99 100
    std::string triggerReq = "CREATE TRIGGER IF NOT EXISTS is_device_present AFTER UPDATE OF is_present ON "
            + policy::DeviceTable::Name +
            " BEGIN"
            " UPDATE " + policy::FolderTable::Name + " SET is_present = new.is_present WHERE device_id = new.id_device;"
            " END";
101 102 103 104
    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)";
105 106 107 108 109
    sqlite::Tools::executeRequest( connection, req );
    sqlite::Tools::executeRequest( connection, exclEntryReq );
    sqlite::Tools::executeRequest( connection, triggerReq );
    sqlite::Tools::executeRequest( connection, deviceIndexReq );
    sqlite::Tools::executeRequest( connection, parentFolderIndexReq );
110 111
}

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

133 134 135 136 137 138
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
139
bool Folder::blacklist( MediaLibraryPtr ml, const std::string& mrl )
140
{
141
    // Ensure we delete the existing folder if any & blacklist the folder in an "atomic" way
142 143
    return sqlite::Tools::withRetries( 3, [ml, &mrl]() {
        auto t = ml->getConn()->newTransaction();
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
        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;
        auto folderFs = fsFactory->createDirectory( mrl );
        assert( folderFs != nullptr );
        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;
    });
179 180
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
181
std::shared_ptr<Folder> Folder::fromMrl( MediaLibraryPtr ml, const std::string& mrl )
182
{
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
183
    return fromMrl( ml, mrl, BannedType::No );
184 185
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
186
std::shared_ptr<Folder> Folder::blacklistedFolder( MediaLibraryPtr ml, const std::string& mrl )
187
{
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
188
    return fromMrl( ml, mrl, BannedType::Yes );
189 190
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
191
std::shared_ptr<Folder> Folder::fromMrl( MediaLibraryPtr ml, const std::string& mrl, BannedType bannedType )
192
{
193 194
    if ( mrl.empty() == true )
        return nullptr;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
195
    auto fsFactory = ml->fsFactoryForMrl( mrl );
196 197
    if ( fsFactory == nullptr )
        return nullptr;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
198
    auto folderFs = fsFactory->createDirectory( mrl );
199
    assert( folderFs != nullptr );
200 201 202
    auto deviceFs = folderFs->device();
    if ( deviceFs == nullptr )
    {
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
203
        LOG_ERROR( "Failed to get device containing an existing folder: ", folderFs->mrl() );
204 205
        return nullptr;
    }
206 207
    if ( deviceFs->isRemovable() == false )
    {
208 209
        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
210
            return fetch( ml, req, folderFs->mrl() );
211
        req += " AND is_blacklisted = ?";
212
        return fetch( ml, req, folderFs->mrl(), bannedType == BannedType::Yes ? true : false );
213 214
    }

215
    auto device = Device::fromUuid( ml, deviceFs->uuid() );
216 217 218
    // 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
219
    auto path = utils::file::removePath( folderFs->mrl(), deviceFs->mountpoint() );
220 221 222 223 224 225 226 227 228 229 230
    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 );
    }
231 232 233
    if ( folder == nullptr )
        return nullptr;
    folder->m_deviceMountpoint = deviceFs->mountpoint();
234
    folder->m_fullPath = folder->m_deviceMountpoint.get() + path;
235
    return folder;
236 237
}

238
int64_t Folder::id() const
239 240 241 242
{
    return m_id;
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
243
const std::string& Folder::mrl() const
244
{
245 246 247
    if ( m_isRemovable == false )
        return m_path;

248 249 250 251
    auto lock = m_deviceMountpoint.lock();
    if ( m_deviceMountpoint.isCached() == true )
        return m_fullPath;

252 253 254 255 256 257 258 259 260 261 262
    // 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() );
263
    assert( fsFactory != nullptr );
264
    auto deviceFs = fsFactory->createDevice( m_device.get()->uuid() );
265 266 267 268 269 270 271 272
    // 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;
    }
273 274
    m_deviceMountpoint = deviceFs->mountpoint();
    m_fullPath = m_deviceMountpoint.get() + m_path;
275
    return m_fullPath;
276 277
}

278
std::vector<std::shared_ptr<File>> Folder::files()
279
{
280
    static const std::string req = "SELECT * FROM " + policy::FileTable::Name +
281
        " WHERE folder_id = ?";
282
    return File::fetchAll<File>( m_ml, req, m_id );
283
}
284

285
std::vector<std::shared_ptr<Folder>> Folder::folders()
286
{
287
    static const std::string req = "SELECT * FROM " + policy::FolderTable::Name
288
            + " WHERE parent_id = ? AND is_blacklisted = 0 AND is_present != 0";
289
    return DatabaseHelpers::fetchAll<Folder>( m_ml, req, m_id );
290 291
}

292
std::shared_ptr<Folder> Folder::parent()
293
{
294
    return fetch( m_ml, m_parent );
295
}
296

297
int64_t Folder::deviceId() const
298
{
299
    return m_deviceId;
300
}
301 302 303

bool Folder::isPresent() const
{
304 305 306
    auto deviceLock = m_device.lock();
    if ( m_device.isCached() == false )
        m_device = Device::fetch( m_ml, m_deviceId );
307 308
    // There must be a device containing the folder, since we never create a folder
    // without a device
309
    assert( m_device.get() != nullptr );
310 311 312
    // However, handle potential sporadic errors gracefully
    if( m_device.get() == nullptr )
        return false;
313
    return m_device.get()->isPresent();
314
}
315

316 317 318 319 320
bool Folder::isRootFolder() const
{
    return m_parent == 0;
}

321
std::vector<std::shared_ptr<Folder>> Folder::fetchRootFolders( MediaLibraryPtr ml )
322
{
323 324 325 326
    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"
327
            " parent_id IS NULL AND is_blacklisted = 0 AND is_present != 0";
328
    return DatabaseHelpers::fetchAll<Folder>( ml, req );
329
}
330 331

}