Folder.cpp 20.3 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 "database/SqliteQuery.h"
34 35
#include "medialibrary/filesystem/IDirectory.h"
#include "medialibrary/filesystem/IDevice.h"
36
#include "medialibrary/filesystem/IFileSystemFactory.h"
37
#include "utils/Filename.h"
38
#include "utils/Url.h"
39 40

#include <unordered_map>
41

42 43 44
namespace medialibrary
{

45 46 47
const std::string Folder::Table::Name = "Folder";
const std::string Folder::Table::PrimaryKeyColumn = "id_folder";
int64_t Folder::* const Folder::Table::PrimaryKey = &Folder::m_id;
48

49 50
Folder::Folder( MediaLibraryPtr ml, sqlite::Row& row )
    : m_ml( ml )
51 52
    , m_id( row.load<decltype(m_id)>( 0 ) )
    , m_path( row.load<decltype(m_path)>( 1 ) )
53 54 55 56 57
    , m_name( row.load<decltype(m_name)>( 2 ) )
    , m_parent( row.load<decltype(m_parent)>( 3 ) )
    , m_isBanned( row.load<decltype(m_isBanned)>( 4 ) )
    , m_deviceId( row.load<decltype(m_deviceId)>( 5 ) )
    , m_isRemovable( row.load<decltype(m_isRemovable)>( 6 ) )
58
    // Skip nb_audio/nb_video
59 60 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_name( utils::url::decode( utils::file::directoryName( path ) ) )
68
    , m_parent( parent )
69
    , m_isBanned( false )
70
    , m_deviceId( deviceId )
71
    , m_isRemovable( isRemovable )
72 73 74
{
}

75
void Folder::createTable( sqlite::Connection* connection)
76
{
77
    const std::string reqs[] = {
78
        #include "database/tables/Folder_v15.sql"
79 80 81
    };
    for ( const auto& req : reqs )
        sqlite::Tools::executeRequest( connection, req );
82 83
}

84
void Folder::createTriggers( sqlite::Connection* connection, uint32_t modelVersion )
85
{
86
    const std::string reqs[] = {
87
        #include "database/tables/Folder_triggers_v15.sql"
88 89 90
    };
    for ( const auto& req : reqs )
        sqlite::Tools::executeRequest( connection, req );
91 92 93 94 95 96 97
    if ( modelVersion >= 14 )
    {
        const std::string v14Reqs[] = {
            "CREATE TRIGGER IF NOT EXISTS update_folder_nb_media_on_insert "
                "AFTER INSERT ON " + Media::Table::Name + " "
                "WHEN new.folder_id IS NOT NULL "
            "BEGIN "
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
                "UPDATE " + Folder::Table::Name + " SET "
                    "nb_audio = nb_audio + "
                        "(CASE new.type "
                            "WHEN " +
                                std::to_string( static_cast<std::underlying_type<IMedia::Type>::type>(
                                    IMedia::Type::Audio ) ) + " THEN 1 "
                            "ELSE 0 "
                        "END),"
                    "nb_video = nb_video + "
                        "(CASE new.type WHEN " +
                            std::to_string( static_cast<std::underlying_type<IMedia::Type>::type>(
                                                IMedia::Type::Video ) ) + " THEN 1 "
                            "ELSE 0 "
                        "END) "
                    "WHERE id_folder = new.folder_id;"
            "END",

            "CREATE TRIGGER IF NOT EXISTS update_folder_nb_media_on_update "
                "AFTER UPDATE ON " + Media::Table::Name + " "
                "WHEN new.folder_id IS NOT NULL AND old.type != new.type "
            "BEGIN "
                "UPDATE " + Folder::Table::Name + " SET "
                    "nb_audio = nb_audio + "
                        "(CASE old.type "
                            "WHEN " +
                                std::to_string( static_cast<std::underlying_type<IMedia::Type>::type>(
                                                IMedia::Type::Audio ) ) + " THEN -1 "
                            "ELSE 0 "
                        "END)"
                        "+"
                        "(CASE new.type "
                            "WHEN " +
                                std::to_string( static_cast<std::underlying_type<IMedia::Type>::type>(
                                                IMedia::Type::Audio ) ) + " THEN 1 "
                            "ELSE 0 "
                        "END)"
                    ","
                    "nb_video = nb_video + "
                        "(CASE old.type "
                            "WHEN " +
                                std::to_string( static_cast<std::underlying_type<IMedia::Type>::type>(
                                                    IMedia::Type::Video ) ) + " THEN -1 "
                            "ELSE 0 "
                        "END)"
                        "+"
                        "(CASE new.type "
                            "WHEN " +
                                std::to_string( static_cast<std::underlying_type<IMedia::Type>::type>(
                                                    IMedia::Type::Video ) ) + " THEN 1 "
                            "ELSE 0 "
                        "END)"
149 150 151 152 153 154 155
                    "WHERE id_folder = new.folder_id;"
            "END",

            "CREATE TRIGGER IF NOT EXISTS update_folder_nb_media_on_delete "
                "AFTER DELETE ON " + Media::Table::Name + " "
                "WHEN old.folder_id IS NOT NULL "
            "BEGIN "
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
                "UPDATE " + Folder::Table::Name + " SET "
                    "nb_audio = nb_audio + "
                        "(CASE old.type "
                            "WHEN " +
                                std::to_string( static_cast<std::underlying_type<IMedia::Type>::type>(
                                                    IMedia::Type::Audio ) ) + " THEN -1 "
                            "ELSE 0 "
                        "END),"
                    "nb_video = nb_video + "
                        "(CASE old.type "
                            "WHEN " +
                                std::to_string( static_cast<std::underlying_type<IMedia::Type>::type>(
                                                    IMedia::Type::Video ) ) + " THEN -1 "
                            "ELSE 0 "
                        "END) "
171 172 173 174 175 176
                    "WHERE id_folder = old.folder_id;"
            "END",
        };
        for ( const auto& req : v14Reqs )
            sqlite::Tools::executeRequest( connection, req );
    }
177 178
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
179
std::shared_ptr<Folder> Folder::create( MediaLibraryPtr ml, const std::string& mrl,
180
                                        int64_t parentId, Device& device, fs::IDevice& deviceFs )
181
{
182 183
    std::string path;
    if ( device.isRemovable() == true )
184
        path = deviceFs.relativeMrl( mrl );
185
    else
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
186
        path = mrl;
187
    auto self = std::make_shared<Folder>( ml, path, parentId, device.id(), device.isRemovable() );
188
    static const std::string req = "INSERT INTO " + Folder::Table::Name +
189 190
            "(path, name, parent_id, device_id, is_removable) VALUES(?, ?, ?, ?, ?)";
    if ( insert( ml, self, req, path, self->m_name, sqlite::ForeignKey( parentId ), device.id(), device.isRemovable() ) == false )
191
        return nullptr;
192
    if ( device.isRemovable() == true )
193
        self->m_fullPath = deviceFs.absoluteMrl( path );
194 195 196
    return self;
}

197 198 199 200 201 202
void Folder::excludeEntryFolder( MediaLibraryPtr ml, int64_t folderId )
{
    std::string req = "INSERT INTO ExcludedEntryFolder(folder_id) VALUES(?)";
    sqlite::Tools::executeRequest( ml->getConn(), req, folderId );
}

203
bool Folder::ban( MediaLibraryPtr ml, const std::string& mrl )
204
{
205
    // Ensure we delete the existing folder if any & ban the folder in an "atomic" way
206 207
    return sqlite::Tools::withRetries( 3, [ml, &mrl]() {
        auto t = ml->getConn()->newTransaction();
208

209 210 211
        auto f = fromMrl( ml, mrl, BannedType::Any );
        if ( f != nullptr )
        {
212 213
            // No need to ban a folder twice
            if ( f->m_isBanned == true )
214 215 216 217 218 219 220
                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;
221 222 223 224 225 226 227 228 229 230
        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;
        }
231 232 233 234 235 236 237 238 239 240 241
        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 )
242
            path = deviceFs->relativeMrl( mrl );
243 244
        else
            path = mrl;
245
        static const std::string req = "INSERT INTO " + Folder::Table::Name +
246
                "(path, parent_id, is_banned, device_id, is_removable) VALUES(?, ?, ?, ?, ?)";
247 248 249 250
        auto res = sqlite::Tools::executeInsert( ml->getConn(), req, path, nullptr, true, device->id(), deviceFs->isRemovable() ) != 0;
        t->commit();
        return res;
    });
251 252
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
253
std::shared_ptr<Folder> Folder::fromMrl( MediaLibraryPtr ml, const std::string& mrl )
254
{
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
255
    return fromMrl( ml, mrl, BannedType::No );
256 257
}

258
std::shared_ptr<Folder> Folder::bannedFolder( MediaLibraryPtr ml, const std::string& mrl )
259
{
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
260
    return fromMrl( ml, mrl, BannedType::Yes );
261 262
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
263
std::shared_ptr<Folder> Folder::fromMrl( MediaLibraryPtr ml, const std::string& mrl, BannedType bannedType )
264
{
265 266
    if ( mrl.empty() == true )
        return nullptr;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
267
    auto fsFactory = ml->fsFactoryForMrl( mrl );
268 269
    if ( fsFactory == nullptr )
        return nullptr;
270 271 272 273 274 275 276 277 278 279 280
    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;
    }

281 282 283
    auto deviceFs = folderFs->device();
    if ( deviceFs == nullptr )
    {
284
        LOG_WARN( "Failed to get device containing an existing folder: ", folderFs->mrl() );
285 286
        return nullptr;
    }
287 288
    if ( deviceFs->isRemovable() == false )
    {
289
        std::string req = "SELECT * FROM " + Folder::Table::Name + " WHERE path = ? AND is_removable = 0";
290
        if ( bannedType == BannedType::Any )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
291
            return fetch( ml, req, folderFs->mrl() );
292
        req += " AND is_banned = ?";
293
        return fetch( ml, req, folderFs->mrl(), bannedType == BannedType::Yes ? true : false );
294 295
    }

296
    auto device = Device::fromUuid( ml, deviceFs->uuid() );
297 298 299
    // 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;
300
    auto path = deviceFs->relativeMrl( folderFs->mrl() );
301
    std::string req = "SELECT * FROM " + Folder::Table::Name + " WHERE path = ? AND device_id = ?";
302 303 304 305 306 307 308
    std::shared_ptr<Folder> folder;
    if ( bannedType == BannedType::Any )
    {
        folder = fetch( ml, req, path, device->id() );
    }
    else
    {
309
        req += " AND is_banned = ?";
310 311
        folder = fetch( ml, req, path, device->id(), bannedType == BannedType::Yes ? true : false );
    }
312 313
    if ( folder == nullptr )
        return nullptr;
314
    folder->m_fullPath = deviceFs->absoluteMrl( path );
315
    return folder;
316 317
}

318 319 320 321 322 323 324
std::string Folder::sortRequest( const QueryParameters* params )
{
    auto sort = params != nullptr ? params->sort : SortingCriteria::Default;
    auto desc = params != nullptr ? params->desc : false;
    std::string req = "ORDER BY ";
    switch ( sort )
    {
325 326 327 328 329 330 331 332
        case SortingCriteria::NbVideo:
            req += "nb_video";
            desc = !desc;
            break;
        case SortingCriteria::NbAudio:
            req += "nb_audio";
            desc = !desc;
            break;
333
        case SortingCriteria::NbMedia:
334
            req += "(nb_audio + nb_video)";
335 336 337 338 339 340 341 342 343 344 345 346 347 348
            desc = !desc;
            break;
        default:
            LOG_WARN( "Unsupported sorting criteria, falling back to Default (alpha)" );
            /* fall-through */
        case SortingCriteria::Default:
        case SortingCriteria::Alpha:
            req += "name";
    }
    if ( desc == true )
        req += " DESC";
    return req;
}

349 350 351 352 353
std::string Folder::filterByMediaType( IMedia::Type type )
{
    switch ( type )
    {
        case IMedia::Type::Audio:
354
            return " f.nb_audio > 0";
355
        case IMedia::Type::Video:
356
            return " f.nb_video > 0";
357 358 359 360
        default:
            assert( !"Only Audio/Video/Unknown types are supported when listing folders" );
            /* Fall-through */
        case IMedia::Type::Unknown:
361
            return " (f.nb_audio > 0 OR f.nb_video > 0)";
362 363 364 365 366
    }
}

Query<IFolder> Folder::withMedia( MediaLibraryPtr ml, IMedia::Type type,
                                  const QueryParameters* params )
367
{
368 369 370 371 372
    std::string req = "FROM " + Table::Name + " f "
                      " LEFT JOIN " + Device::Table::Name +
                            " d ON d.id_device = f.device_id "
                      " WHERE " + filterByMediaType( type ) +
                            " AND d.is_present != 0";
373 374 375 376
    return make_query<Folder, IFolder>( ml, "*", req, sortRequest( params ) );
}

Query<IFolder> Folder::searchWithMedia( MediaLibraryPtr ml, const std::string& pattern,
377
                                        IMedia::Type type, const QueryParameters* params )
378
{
379 380 381 382 383 384
    std::string req = "FROM " + Table::Name + " f "
            " LEFT JOIN " + Device::Table::Name +
                " d ON d.id_device = f.device_id "
            "WHERE f.id_folder IN (SELECT rowid FROM " + Table::Name + "Fts WHERE " +
                Table::Name + "Fts MATCH '*' || ? || '*') "
            "AND d.is_present != 0 AND " + filterByMediaType( type );
385 386 387
    return make_query<Folder, IFolder>( ml, "*", req, sortRequest( params ), pattern );
}

388 389 390 391 392 393 394 395 396 397
Query<IFolder> Folder::entryPoints( MediaLibraryPtr ml, int64_t deviceId )
{
    std::string req = "FROM " + Folder::Table::Name + " WHERE parent_id IS NULL"
            " AND is_banned = 0";
    if ( deviceId == 0 )
        return make_query<Folder, IFolder>( ml, "*", req, "" );
    req += " AND device_id = ?";
    return make_query<Folder, IFolder>( ml, "*", req, "", sqlite::ForeignKey{ deviceId } );
}

398
int64_t Folder::id() const
399 400 401 402
{
    return m_id;
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
403
const std::string& Folder::mrl() const
404
{
405 406 407
    if ( m_isRemovable == false )
        return m_path;

408
    if ( m_fullPath.empty() == false )
409 410
        return m_fullPath;

411 412 413 414
    // 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 )
415
        throw fs::DeviceRemovedException();
416

417
    auto fsFactory = m_ml->fsFactoryForMrl( m_device->scheme() );
418 419 420 421 422 423
    if ( fsFactory == nullptr )
    {
        assert( !"Failed to find a FileSystemFactory for a known folder" );
        m_fullPath = "";
        return m_fullPath;
    }
424
    auto deviceFs = fsFactory->createDevice( m_device->uuid() );
425 426 427 428 429 430 431 432
    // 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;
    }
433
    m_fullPath = deviceFs->absoluteMrl( m_path );
434
    return m_fullPath;
435 436
}

437 438
const std::string&Folder::name() const
{
439 440 441 442 443 444 445 446
    if ( m_isRemovable == true && m_name.empty() == true )
    {
        // In case this is the root folder of an external device, we don't have
        // any information before knowing the actual mountpoint, so we have to do
        // this at runtime
        auto fullPath = mrl();
        m_name = utils::url::decode( utils::file::directoryName( fullPath ) );
    }
447 448 449 450 451 452 453 454
    return m_name;
}

void Folder::setName( std::string name )
{
    assert( m_name.empty() == true );
    static const std::string req = "UPDATE " + Table::Name +
            " SET name = ? WHERE id_folder = ?";
455 456
    auto dbConn = m_ml->getConn();
    if ( sqlite::Tools::executeUpdate( dbConn, req, name, m_id ) == false )
457
        return;
458 459 460
    static const std::string reqFts = "INSERT INTO " + Table::Name + "Fts "
            "(rowid, name) VALUES(?, ?)";
    sqlite::Tools::executeInsert( dbConn, reqFts, m_id, name );
461 462 463
    m_name = std::move( name );
}

464 465 466 467 468 469 470 471 472
const std::string& Folder::rawMrl() const
{
    return m_path;
}

void Folder::setMrl( std::string mrl )
{
    if ( m_path == mrl )
        return;
473
    static const std::string req = "UPDATE " + Folder::Table::Name + " SET "
474 475 476 477 478 479 480 481 482 483
            "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 );
}

484
std::vector<std::shared_ptr<File>> Folder::files()
485
{
486
    static const std::string req = "SELECT * FROM " + File::Table::Name +
487
        " WHERE folder_id = ?";
488
    return File::fetchAll<File>( m_ml, req, m_id );
489
}
490

491
std::vector<std::shared_ptr<Folder>> Folder::folders()
492
{
493 494
    static const std::string req = "SELECT * FROM " + Folder::Table::Name + " f "
            " LEFT JOIN " + Device::Table::Name + " d ON d.id_device = f.device_id"
495
            " WHERE parent_id = ? AND is_banned = 0 AND d.is_present != 0";
496
    return DatabaseHelpers::fetchAll<Folder>( m_ml, req, m_id );
497 498
}

499
std::shared_ptr<Folder> Folder::parent()
500
{
501
    return fetch( m_ml, m_parent );
502
}
503

504
int64_t Folder::deviceId() const
505
{
506
    return m_deviceId;
507
}
508

509 510 511 512 513
bool Folder::isRemovable() const
{
    return m_isRemovable;
}

514 515
bool Folder::isPresent() const
{
516
    if ( m_device == nullptr )
517
        m_device = Device::fetch( m_ml, m_deviceId );
518 519
    // There must be a device containing the folder, since we never create a folder
    // without a device
520
    assert( m_device != nullptr );
521
    // However, handle potential sporadic errors gracefully
522
    if( m_device == nullptr )
523
        return false;
524
    return m_device->isPresent();
525
}
526

527 528
bool Folder::isBanned() const
{
529
    return m_isBanned;
530 531
}

532 533 534 535 536
bool Folder::isRootFolder() const
{
    return m_parent == 0;
}

537 538 539 540 541 542
Query<IMedia> Folder::media( IMedia::Type type, const QueryParameters* params ) const
{
    return Media::fromFolderId( m_ml, type, m_id, params );
}

Query<IFolder> Folder::subfolders( const QueryParameters* params ) const
543
{
544 545
    static const std::string req = "FROM " + Table::Name + " WHERE parent_id = ?";
    return make_query<Folder, IFolder>( m_ml, "*", req, sortRequest( params ), m_id );
546 547
}

548
std::vector<std::shared_ptr<Folder>> Folder::fetchRootFolders( MediaLibraryPtr ml )
549
{
550
    static const std::string req = "SELECT * FROM " + Folder::Table::Name + " f "
551
            " LEFT JOIN ExcludedEntryFolder"
552 553
            " ON f.id_folder = ExcludedEntryFolder.folder_id"
            " LEFT JOIN " + Device::Table::Name + " d ON d.id_device = f.device_id"
554
            " WHERE ExcludedEntryFolder.folder_id IS NULL AND"
555
            " parent_id IS NULL AND is_banned = 0 AND d.is_present != 0";
556
    return DatabaseHelpers::fetchAll<Folder>( ml, req );
557
}
558 559

}