mirror of https://github.com/gnosygnu/xowa
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3126 lines
93 KiB
3126 lines
93 KiB
/*
|
|
XOWA: the XOWA Offline Wiki Application
|
|
Copyright (C) 2012-2017 gnosygnu@gmail.com
|
|
|
|
XOWA is licensed under the terms of the General Public License (GPL) Version 3,
|
|
or alternatively under the terms of the Apache License Version 2.0.
|
|
|
|
You may use XOWA according to either of these licenses as is most appropriate
|
|
for your project on a case-by-case basis.
|
|
|
|
The terms of each license can be found in the source code repository:
|
|
|
|
GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
|
|
Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
|
|
*/
|
|
package gplx.xowa.mediawiki.includes.filerepo.file; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.includes.*; import gplx.xowa.mediawiki.includes.filerepo.*;
|
|
import gplx.xowa.mediawiki.includes.parsers.*;
|
|
public class XomwLocalFile extends XomwFile {// static final VERSION = 10; // cache version
|
|
//
|
|
// static final CACHE_FIELD_MAX_LEN = 1000;
|
|
//
|
|
// /** @var boolean Does the file exist on disk? (loadFromXxx) */
|
|
// protected fileExists;
|
|
|
|
/** @var int Image width */
|
|
public int width;
|
|
|
|
/** @var int Image height */
|
|
public int height;
|
|
|
|
// /** @var int Returned by getimagesize (loadFromXxx) */
|
|
// protected bits;
|
|
//
|
|
// /** @var String MEDIATYPE_xxx (bitmap, drawing, audio...) */
|
|
// protected media_type;
|
|
|
|
/** @var String MIME type, determined by MimeMagic::guessMimeType */
|
|
private byte[] mime;
|
|
|
|
// /** @var int Size in bytes (loadFromXxx) */
|
|
// protected size;
|
|
//
|
|
// /** @var String Handler-specific metadata */
|
|
// protected metadata;
|
|
//
|
|
// /** @var String SHA-1 super 36 content hash */
|
|
// protected sha1;
|
|
//
|
|
// /** @var boolean Whether or not core data has been loaded from the database (loadFromXxx) */
|
|
// protected dataLoaded;
|
|
//
|
|
// /** @var boolean Whether or not lazy-loaded data has been loaded from the database */
|
|
// protected extraDataLoaded;
|
|
//
|
|
// /** @var int Bitfield akin to rev_deleted */
|
|
// protected deleted;
|
|
//
|
|
// /** @var String */
|
|
// protected repoClass = 'LocalRepo';
|
|
//
|
|
// /** @var int Number of line to return by nextHistoryLine() (constructor) */
|
|
// private historyLine;
|
|
//
|
|
// /** @var int Result of the query for the file's history (nextHistoryLine) */
|
|
// private historyRes;
|
|
//
|
|
// /** @var String Major MIME type */
|
|
// private major_mime;
|
|
//
|
|
// /** @var String Minor MIME type */
|
|
// private minor_mime;
|
|
//
|
|
// /** @var String Upload timestamp */
|
|
// private timestamp;
|
|
//
|
|
// /** @var int User ID of uploader */
|
|
// private user;
|
|
//
|
|
// /** @var String User name of uploader */
|
|
// private user_text;
|
|
//
|
|
// /** @var String Description of current revision of the file */
|
|
// private description;
|
|
//
|
|
// /** @var String TS_MW timestamp of the last change of the file description */
|
|
// private descriptionTouched;
|
|
//
|
|
// /** @var boolean Whether the row was upgraded on load */
|
|
// private upgraded;
|
|
//
|
|
// /** @var boolean Whether the row was scheduled to upgrade on load */
|
|
// private upgrading;
|
|
//
|
|
// /** @var boolean True if the image row is locked */
|
|
// private locked;
|
|
//
|
|
// /** @var boolean True if the image row is locked with a synchronized initiated transaction */
|
|
// private lockedOwnTrx;
|
|
//
|
|
// /** @var boolean True if file is not present in file system. Not to be cached in memcached */
|
|
// private missing;
|
|
//
|
|
// // @note: higher than IDBAccessObject constants
|
|
// static final LOAD_ALL = 16; // integer; load all the lazy fields too (like metadata)
|
|
//
|
|
// static final ATOMIC_SECTION_LOCK = 'LocalFile::lockingTransaction';
|
|
//
|
|
// /**
|
|
// * Create a LocalFile from a title
|
|
// * Do not call this except from inside a repo class.
|
|
// *
|
|
// * Note: unused param is only here to avoid an E_STRICT
|
|
// *
|
|
// * @param Title title
|
|
// * @param FileRepo repo
|
|
// * @param null unused
|
|
// *
|
|
// * @return LocalFile
|
|
// */
|
|
// static function newFromTitle( title, repo, unused = null ) {
|
|
// return new self( title, repo );
|
|
// }
|
|
//
|
|
// /**
|
|
// * Create a LocalFile from a title
|
|
// * Do not call this except from inside a repo class.
|
|
// *
|
|
// * @param stdClass row
|
|
// * @param FileRepo repo
|
|
// *
|
|
// * @return LocalFile
|
|
// */
|
|
// static function newFromRow( row, repo ) {
|
|
// title = Title::makeTitle( NS_FILE, row.img_name );
|
|
// file = new self( title, repo );
|
|
// file.loadFromRow( row );
|
|
//
|
|
// return file;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Create a LocalFile from a SHA-1 key
|
|
// * Do not call this except from inside a repo class.
|
|
// *
|
|
// * @param String sha1 Base-36 SHA-1
|
|
// * @param LocalRepo repo
|
|
// * @param String|boolean timestamp MW_timestamp (optional)
|
|
// * @return boolean|LocalFile
|
|
// */
|
|
// static function newFromKey( sha1, repo, timestamp = false ) {
|
|
// dbr = repo.getReplicaDB();
|
|
//
|
|
// conds = [ 'img_sha1' => sha1 ];
|
|
// if ( timestamp ) {
|
|
// conds['img_timestamp'] = dbr.timestamp( timestamp );
|
|
// }
|
|
//
|
|
// row = dbr.selectRow( 'image', self::selectFields(), conds, __METHOD__ );
|
|
// if ( row ) {
|
|
// return self::newFromRow( row, repo );
|
|
// } else {
|
|
// return false;
|
|
// }
|
|
// }
|
|
//
|
|
// /**
|
|
// * Fields in the image table
|
|
// * @return array
|
|
// */
|
|
// static function selectFields() {
|
|
// return [
|
|
// 'img_name',
|
|
// 'img_size',
|
|
// 'img_width',
|
|
// 'img_height',
|
|
// 'img_metadata',
|
|
// 'img_bits',
|
|
// 'img_media_type',
|
|
// 'img_major_mime',
|
|
// 'img_minor_mime',
|
|
// 'img_description',
|
|
// 'img_user',
|
|
// 'img_user_text',
|
|
// 'img_timestamp',
|
|
// 'img_sha1',
|
|
// ];
|
|
// }
|
|
|
|
public XomwLocalFile(XomwEnv env, XomwTitleOld title, XomwFileRepo repo, int w, int h, byte[] mime) {super(env, title, repo);
|
|
this.width = w;
|
|
this.height = h;
|
|
this.mime = mime;
|
|
}
|
|
// /**
|
|
// * Constructor.
|
|
// * Do not call this except from inside a repo class.
|
|
// * @param Title title
|
|
// * @param FileRepo repo
|
|
// */
|
|
// function __construct( title, repo ) {
|
|
// parent::__construct( title, repo );
|
|
//
|
|
// this.metadata = '';
|
|
// this.historyLine = 0;
|
|
// this.historyRes = null;
|
|
// this.dataLoaded = false;
|
|
// this.extraDataLoaded = false;
|
|
//
|
|
// this.assertRepoDefined();
|
|
// this.assertTitleDefined();
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get the memcached key for the main data for this file, or false if
|
|
// * there is no access to the shared cache.
|
|
// * @return String|boolean
|
|
// */
|
|
// function getCacheKey() {
|
|
// return this.repo.getSharedCacheKey( 'file', sha1( this.getName() ) );
|
|
// }
|
|
//
|
|
// /**
|
|
// * Try to load file metadata from memcached, falling back to the database
|
|
// */
|
|
// private function loadFromCache() {
|
|
// this.dataLoaded = false;
|
|
// this.extraDataLoaded = false;
|
|
//
|
|
// key = this.getCacheKey();
|
|
// if ( !key ) {
|
|
// this.loadFromDB( self::READ_NORMAL );
|
|
//
|
|
// return;
|
|
// }
|
|
//
|
|
// cache = ObjectCache::getMainWANInstance();
|
|
// cachedValues = cache.getWithSetCallback(
|
|
// key,
|
|
// cache::TTL_WEEK,
|
|
// function ( oldValue, &ttl, array &setOpts ) use ( cache ) {
|
|
// setOpts += Database::getCacheSetOptions( this.repo.getReplicaDB() );
|
|
//
|
|
// this.loadFromDB( self::READ_NORMAL );
|
|
//
|
|
// fields = this.getCacheFields( '' );
|
|
// cacheVal['fileExists'] = this.fileExists;
|
|
// if ( this.fileExists ) {
|
|
// foreach ( fields as field ) {
|
|
// cacheVal[field] = this.field;
|
|
// }
|
|
// }
|
|
// // Strip off excessive entries from the subset of fields that can become large.
|
|
// // If the cache value gets to large it will not fit in memcached and nothing will
|
|
// // get cached at all, causing master queries for any file access.
|
|
// foreach ( this.getLazyCacheFields( '' ) as field ) {
|
|
// if ( isset( cacheVal[field] )
|
|
// && strlen( cacheVal[field] ) > 100 * 1024
|
|
// ) {
|
|
// unset( cacheVal[field] ); // don't let the value get too big
|
|
// }
|
|
// }
|
|
//
|
|
// if ( this.fileExists ) {
|
|
// ttl = cache.adaptiveTTL( wfTimestamp( TS_UNIX, this.timestamp ), ttl );
|
|
// } else {
|
|
// ttl = cache::TTL_DAY;
|
|
// }
|
|
//
|
|
// return cacheVal;
|
|
// },
|
|
// [ 'version' => self::VERSION ]
|
|
// );
|
|
//
|
|
// this.fileExists = cachedValues['fileExists'];
|
|
// if ( this.fileExists ) {
|
|
// this.setProps( cachedValues );
|
|
// }
|
|
//
|
|
// this.dataLoaded = true;
|
|
// this.extraDataLoaded = true;
|
|
// foreach ( this.getLazyCacheFields( '' ) as field ) {
|
|
// this.extraDataLoaded = this.extraDataLoaded && isset( cachedValues[field] );
|
|
// }
|
|
// }
|
|
//
|
|
// /**
|
|
// * Purge the file Object/metadata cache
|
|
// */
|
|
// public function invalidateCache() {
|
|
// key = this.getCacheKey();
|
|
// if ( !key ) {
|
|
// return;
|
|
// }
|
|
//
|
|
// this.repo.getMasterDB().onTransactionPreCommitOrIdle(
|
|
// function () use ( key ) {
|
|
// ObjectCache::getMainWANInstance().delete( key );
|
|
// },
|
|
// __METHOD__
|
|
// );
|
|
// }
|
|
//
|
|
// /**
|
|
// * Load metadata from the file itself
|
|
// */
|
|
// function loadFromFile() {
|
|
// props = this.repo.getFileProps( this.getVirtualUrl() );
|
|
// this.setProps( props );
|
|
// }
|
|
//
|
|
// /**
|
|
// * @param String prefix
|
|
// * @return array
|
|
// */
|
|
// function getCacheFields( prefix = 'img_' ) {
|
|
// static fields = [ 'size', 'width', 'height', 'bits', 'media_type',
|
|
// 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user',
|
|
// 'user_text', 'description' ];
|
|
// static results = [];
|
|
//
|
|
// if ( prefix == '' ) {
|
|
// return fields;
|
|
// }
|
|
//
|
|
// if ( !isset( results[prefix] ) ) {
|
|
// prefixedFields = [];
|
|
// foreach ( fields as field ) {
|
|
// prefixedFields[] = prefix . field;
|
|
// }
|
|
// results[prefix] = prefixedFields;
|
|
// }
|
|
//
|
|
// return results[prefix];
|
|
// }
|
|
//
|
|
// /**
|
|
// * @param String prefix
|
|
// * @return array
|
|
// */
|
|
// function getLazyCacheFields( prefix = 'img_' ) {
|
|
// static fields = [ 'metadata' ];
|
|
// static results = [];
|
|
//
|
|
// if ( prefix == '' ) {
|
|
// return fields;
|
|
// }
|
|
//
|
|
// if ( !isset( results[prefix] ) ) {
|
|
// prefixedFields = [];
|
|
// foreach ( fields as field ) {
|
|
// prefixedFields[] = prefix . field;
|
|
// }
|
|
// results[prefix] = prefixedFields;
|
|
// }
|
|
//
|
|
// return results[prefix];
|
|
// }
|
|
//
|
|
// /**
|
|
// * Load file metadata from the DB
|
|
// * @param int flags
|
|
// */
|
|
// function loadFromDB( flags = 0 ) {
|
|
// fname = get_class( this ) . '::' . __FUNCTION__;
|
|
//
|
|
// # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
|
|
// this.dataLoaded = true;
|
|
// this.extraDataLoaded = true;
|
|
//
|
|
// dbr = ( flags & self::READ_LATEST )
|
|
// ? this.repo.getMasterDB()
|
|
// : this.repo.getReplicaDB();
|
|
//
|
|
// row = dbr.selectRow( 'image', this.getCacheFields( 'img_' ),
|
|
// [ 'img_name' => this.getName() ], fname );
|
|
//
|
|
// if ( row ) {
|
|
// this.loadFromRow( row );
|
|
// } else {
|
|
// this.fileExists = false;
|
|
// }
|
|
// }
|
|
//
|
|
// /**
|
|
// * Load lazy file metadata from the DB.
|
|
// * This covers fields that are sometimes not cached.
|
|
// */
|
|
// protected function loadExtraFromDB() {
|
|
// fname = get_class( this ) . '::' . __FUNCTION__;
|
|
//
|
|
// # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
|
|
// this.extraDataLoaded = true;
|
|
//
|
|
// fieldMap = this.loadFieldsWithTimestamp( this.repo.getReplicaDB(), fname );
|
|
// if ( !fieldMap ) {
|
|
// fieldMap = this.loadFieldsWithTimestamp( this.repo.getMasterDB(), fname );
|
|
// }
|
|
//
|
|
// if ( fieldMap ) {
|
|
// foreach ( fieldMap as name => value ) {
|
|
// this.name = value;
|
|
// }
|
|
// } else {
|
|
// throw new MWException( "Could not find data for image '{this.getName()}'." );
|
|
// }
|
|
// }
|
|
//
|
|
// /**
|
|
// * @param IDatabase dbr
|
|
// * @param String fname
|
|
// * @return array|boolean
|
|
// */
|
|
// private function loadFieldsWithTimestamp( dbr, fname ) {
|
|
// fieldMap = false;
|
|
//
|
|
// row = dbr.selectRow( 'image', this.getLazyCacheFields( 'img_' ), [
|
|
// 'img_name' => this.getName(),
|
|
// 'img_timestamp' => dbr.timestamp( this.getTimestamp() )
|
|
// ], fname );
|
|
// if ( row ) {
|
|
// fieldMap = this.unprefixRow( row, 'img_' );
|
|
// } else {
|
|
// # File may have been uploaded over in the meantime; check the old versions
|
|
// row = dbr.selectRow( 'oldimage', this.getLazyCacheFields( 'oi_' ), [
|
|
// 'oi_name' => this.getName(),
|
|
// 'oi_timestamp' => dbr.timestamp( this.getTimestamp() )
|
|
// ], fname );
|
|
// if ( row ) {
|
|
// fieldMap = this.unprefixRow( row, 'oi_' );
|
|
// }
|
|
// }
|
|
//
|
|
// return fieldMap;
|
|
// }
|
|
//
|
|
// /**
|
|
// * @param array|Object row
|
|
// * @param String prefix
|
|
// * @throws MWException
|
|
// * @return array
|
|
// */
|
|
// protected function unprefixRow( row, prefix = 'img_' ) {
|
|
// array = (array)row;
|
|
// prefixLength = strlen( prefix );
|
|
//
|
|
// // Sanity check prefix once
|
|
// if ( substr( key( array ), 0, prefixLength ) !== prefix ) {
|
|
// throw new MWException( __METHOD__ . ': incorrect prefix parameter' );
|
|
// }
|
|
//
|
|
// decoded = [];
|
|
// foreach ( array as name => value ) {
|
|
// decoded[substr( name, prefixLength )] = value;
|
|
// }
|
|
//
|
|
// return decoded;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Decode a row from the database (either Object or array) to an array
|
|
// * with timestamps and MIME types decoded, and the field prefix removed.
|
|
// * @param Object row
|
|
// * @param String prefix
|
|
// * @throws MWException
|
|
// * @return array
|
|
// */
|
|
// function decodeRow( row, prefix = 'img_' ) {
|
|
// decoded = this.unprefixRow( row, prefix );
|
|
//
|
|
// decoded['timestamp'] = wfTimestamp( TS_MW, decoded['timestamp'] );
|
|
//
|
|
// decoded['metadata'] = this.repo.getReplicaDB().decodeBlob( decoded['metadata'] );
|
|
//
|
|
// if ( empty( decoded['major_mime'] ) ) {
|
|
// decoded['mime'] = 'unknown/unknown';
|
|
// } else {
|
|
// if ( !decoded['minor_mime'] ) {
|
|
// decoded['minor_mime'] = 'unknown';
|
|
// }
|
|
// decoded['mime'] = decoded['major_mime'] . '/' . decoded['minor_mime'];
|
|
// }
|
|
//
|
|
// // Trim zero padding from char/binary field
|
|
// decoded['sha1'] = rtrim( decoded['sha1'], "\0" );
|
|
//
|
|
// // Normalize some fields to integer type, per their database definition.
|
|
// // Use unary + so that overflows will be upgraded to double instead of
|
|
// // being trucated as with intval(). This is important to allow >2GB
|
|
// // files on 32-bit systems.
|
|
// foreach ( [ 'size', 'width', 'height', 'bits' ] as field ) {
|
|
// decoded[field] = +decoded[field];
|
|
// }
|
|
//
|
|
// return decoded;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Load file metadata from a DB result row
|
|
// *
|
|
// * @param Object row
|
|
// * @param String prefix
|
|
// */
|
|
// function loadFromRow( row, prefix = 'img_' ) {
|
|
// this.dataLoaded = true;
|
|
// this.extraDataLoaded = true;
|
|
//
|
|
// array = this.decodeRow( row, prefix );
|
|
//
|
|
// foreach ( array as name => value ) {
|
|
// this.name = value;
|
|
// }
|
|
//
|
|
// this.fileExists = true;
|
|
// this.maybeUpgradeRow();
|
|
// }
|
|
//
|
|
// /**
|
|
// * Load file metadata from cache or DB, unless already loaded
|
|
// * @param int flags
|
|
// */
|
|
// function load( flags = 0 ) {
|
|
// if ( !this.dataLoaded ) {
|
|
// if ( flags & self::READ_LATEST ) {
|
|
// this.loadFromDB( flags );
|
|
// } else {
|
|
// this.loadFromCache();
|
|
// }
|
|
// }
|
|
//
|
|
// if ( ( flags & self::LOAD_ALL ) && !this.extraDataLoaded ) {
|
|
// // @note: loads on name/timestamp to reduce race condition problems
|
|
// this.loadExtraFromDB();
|
|
// }
|
|
// }
|
|
//
|
|
// /**
|
|
// * Upgrade a row if it needs it
|
|
// */
|
|
// function maybeUpgradeRow() {
|
|
// global wgUpdateCompatibleMetadata;
|
|
//
|
|
// if ( wfReadOnly() || this.upgrading ) {
|
|
// return;
|
|
// }
|
|
//
|
|
// upgrade = false;
|
|
// if ( is_null( this.media_type ) || this.mime == 'image/svg' ) {
|
|
// upgrade = true;
|
|
// } else {
|
|
// handler = this.getHandler();
|
|
// if ( handler ) {
|
|
// validity = handler.isMetadataValid( this, this.getMetadata() );
|
|
// if ( validity === MediaHandler::METADATA_BAD ) {
|
|
// upgrade = true;
|
|
// } elseif ( validity === MediaHandler::METADATA_COMPATIBLE ) {
|
|
// upgrade = wgUpdateCompatibleMetadata;
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// if ( upgrade ) {
|
|
// this.upgrading = true;
|
|
// // Defer updates unless in auto-commit CLI mode
|
|
// DeferredUpdates::addCallableUpdate( function() {
|
|
// this.upgrading = false; // avoid duplicate updates
|
|
// try {
|
|
// this.upgradeRow();
|
|
// } catch ( LocalFileLockError e ) {
|
|
// // let the other process handle it (or do it next time)
|
|
// }
|
|
// } );
|
|
// }
|
|
// }
|
|
//
|
|
// /**
|
|
// * @return boolean Whether upgradeRow() ran for this Object
|
|
// */
|
|
// function getUpgraded() {
|
|
// return this.upgraded;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Fix assorted version-related problems with the image row by reloading it from the file
|
|
// */
|
|
// function upgradeRow() {
|
|
// this.synchronized(); // begin
|
|
//
|
|
// this.loadFromFile();
|
|
//
|
|
// # Don't destroy file info of missing files
|
|
// if ( !this.fileExists ) {
|
|
// this.unlock();
|
|
// wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
|
|
//
|
|
// return;
|
|
// }
|
|
//
|
|
// dbw = this.repo.getMasterDB();
|
|
// list( major, minor ) = self::splitMime( this.mime );
|
|
//
|
|
// if ( wfReadOnly() ) {
|
|
// this.unlock();
|
|
//
|
|
// return;
|
|
// }
|
|
// wfDebug( __METHOD__ . ': upgrading ' . this.getName() . " to the current schema\n" );
|
|
//
|
|
// dbw.update( 'image',
|
|
// [
|
|
// 'img_size' => this.size, // sanity
|
|
// 'img_width' => this.width,
|
|
// 'img_height' => this.height,
|
|
// 'img_bits' => this.bits,
|
|
// 'img_media_type' => this.media_type,
|
|
// 'img_major_mime' => major,
|
|
// 'img_minor_mime' => minor,
|
|
// 'img_metadata' => dbw.encodeBlob( this.metadata ),
|
|
// 'img_sha1' => this.sha1,
|
|
// ],
|
|
// [ 'img_name' => this.getName() ],
|
|
// __METHOD__
|
|
// );
|
|
//
|
|
// this.invalidateCache();
|
|
//
|
|
// this.unlock(); // done
|
|
// this.upgraded = true; // avoid rework/retries
|
|
// }
|
|
//
|
|
// /**
|
|
// * Set properties in this Object to be equal to those given in the
|
|
// * associative array info. Only cacheable fields can be set.
|
|
// * All fields *must* be set in info except for getLazyCacheFields().
|
|
// *
|
|
// * If 'mime' is given, it will be split into major_mime/minor_mime.
|
|
// * If major_mime/minor_mime are given, this.mime will also be set.
|
|
// *
|
|
// * @param array info
|
|
// */
|
|
// function setProps( info ) {
|
|
// this.dataLoaded = true;
|
|
// fields = this.getCacheFields( '' );
|
|
// fields[] = 'fileExists';
|
|
//
|
|
// foreach ( fields as field ) {
|
|
// if ( isset( info[field] ) ) {
|
|
// this.field = info[field];
|
|
// }
|
|
// }
|
|
//
|
|
// // Fix up mime fields
|
|
// if ( isset( info['major_mime'] ) ) {
|
|
// this.mime = "{info['major_mime']}/{info['minor_mime']}";
|
|
// } elseif ( isset( info['mime'] ) ) {
|
|
// this.mime = info['mime'];
|
|
// list( this.major_mime, this.minor_mime ) = self::splitMime( this.mime );
|
|
// }
|
|
// }
|
|
//
|
|
// /** splitMime inherited */
|
|
// /** getName inherited */
|
|
// /** getTitle inherited */
|
|
// /** getURL inherited */
|
|
// /** getViewURL inherited */
|
|
// /** getPath inherited */
|
|
// /** isVisible inherited */
|
|
//
|
|
// /**
|
|
// * @return boolean
|
|
// */
|
|
// function isMissing() {
|
|
// if ( this.missing === null ) {
|
|
// list( fileExists ) = this.repo.fileExists( this.getVirtualUrl() );
|
|
// this.missing = !fileExists;
|
|
// }
|
|
//
|
|
// return this.missing;
|
|
// }
|
|
|
|
/**
|
|
* Return the width of the image
|
|
*
|
|
* @param int page
|
|
* @return int
|
|
*/
|
|
@Override public int getWidth(int page) {
|
|
// this.load();
|
|
//
|
|
// if ( this.isMultipage() ) {
|
|
// handler = this.getHandler();
|
|
// if ( !handler ) {
|
|
// return 0;
|
|
// }
|
|
// dim = handler.getPageDimensions( this, page );
|
|
// if ( dim ) {
|
|
// return dim['width'];
|
|
// } else {
|
|
// // For non-paged media, the false goes through an
|
|
// // intval, turning failure into 0, so do same here.
|
|
// return 0;
|
|
// }
|
|
// } else {
|
|
return this.width;
|
|
// }
|
|
}
|
|
|
|
/**
|
|
* Return the height of the image
|
|
*
|
|
* @param int page
|
|
* @return int
|
|
*/
|
|
@Override public int getHeight(int page) {
|
|
// this.load();
|
|
//
|
|
// if ( this.isMultipage() ) {
|
|
// handler = this.getHandler();
|
|
// if ( !handler ) {
|
|
// return 0;
|
|
// }
|
|
// dim = handler.getPageDimensions( this, page );
|
|
// if ( dim ) {
|
|
// return dim['height'];
|
|
// } else {
|
|
// // For non-paged media, the false goes through an
|
|
// // intval, turning failure into 0, so do same here.
|
|
// return 0;
|
|
// }
|
|
// } else {
|
|
return this.height;
|
|
// }
|
|
}
|
|
//
|
|
// /**
|
|
// * Returns ID or name of user who uploaded the file
|
|
// *
|
|
// * @param String type 'text' or 'id'
|
|
// * @return int|String
|
|
// */
|
|
// function getUser( type = 'text' ) {
|
|
// this.load();
|
|
//
|
|
// if ( type == 'text' ) {
|
|
// return this.user_text;
|
|
// } else { // id
|
|
// return (int)this.user;
|
|
// }
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get short description URL for a file based on the page ID.
|
|
// *
|
|
// * @return String|null
|
|
// * @throws MWException
|
|
// * @since 1.27
|
|
// */
|
|
// public function getDescriptionShortUrl() {
|
|
// pageId = this.title.getArticleID();
|
|
//
|
|
// if ( pageId !== null ) {
|
|
// url = this.repo.makeUrl( [ 'curid' => pageId ] );
|
|
// if ( url !== false ) {
|
|
// return url;
|
|
// }
|
|
// }
|
|
// return null;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get handler-specific metadata
|
|
// * @return String
|
|
// */
|
|
// function getMetadata() {
|
|
// this.load( self::LOAD_ALL ); // large metadata is loaded in another step
|
|
// return this.metadata;
|
|
// }
|
|
//
|
|
// /**
|
|
// * @return int
|
|
// */
|
|
// function getBitDepth() {
|
|
// this.load();
|
|
//
|
|
// return (int)this.bits;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Returns the size of the image file, in bytes
|
|
// * @return int
|
|
// */
|
|
// public function getSize() {
|
|
// this.load();
|
|
//
|
|
// return this.size;
|
|
// }
|
|
|
|
/**
|
|
* Returns the MIME type of the file.
|
|
* @return String
|
|
*/
|
|
@Override public byte[] getMimeType() {
|
|
// this.load();
|
|
|
|
return this.mime;
|
|
}
|
|
|
|
// /**
|
|
// * Returns the type of the media in the file.
|
|
// * Use the value returned by this function with the MEDIATYPE_xxx constants.
|
|
// * @return String
|
|
// */
|
|
// function getMediaType() {
|
|
// this.load();
|
|
//
|
|
// return this.media_type;
|
|
// }
|
|
//
|
|
// /** canRender inherited */
|
|
// /** mustRender inherited */
|
|
// /** allowInlineDisplay inherited */
|
|
// /** isSafeFile inherited */
|
|
// /** isTrustedFile inherited */
|
|
//
|
|
// /**
|
|
// * Returns true if the file exists on disk.
|
|
// * @return boolean Whether file exist on disk.
|
|
// */
|
|
// public function exists() {
|
|
// this.load();
|
|
//
|
|
// return this.fileExists;
|
|
// }
|
|
//
|
|
// /** getTransformScript inherited */
|
|
// /** getUnscaledThumb inherited */
|
|
// /** thumbName inherited */
|
|
// /** createThumb inherited */
|
|
// /** transform inherited */
|
|
//
|
|
// /** getHandler inherited */
|
|
// /** iconThumb inherited */
|
|
// /** getLastError inherited */
|
|
//
|
|
// /**
|
|
// * Get all thumbnail names previously generated for this file
|
|
// * @param String|boolean archiveName Name of an archive file, default false
|
|
// * @return array First element is the super dir, then files in that super dir.
|
|
// */
|
|
// function getThumbnails( archiveName = false ) {
|
|
// if ( archiveName ) {
|
|
// dir = this.getArchiveThumbPath( archiveName );
|
|
// } else {
|
|
// dir = this.getThumbPath();
|
|
// }
|
|
//
|
|
// backend = this.repo.getBackend();
|
|
// files = [ dir ];
|
|
// try {
|
|
// iterator = backend.getFileList( [ 'dir' => dir ] );
|
|
// foreach ( iterator as file ) {
|
|
// files[] = file;
|
|
// }
|
|
// } catch ( FileBackendError e ) {
|
|
// } // suppress (bug 54674)
|
|
//
|
|
// return files;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Refresh metadata in memcached, but don't touch thumbnails or CDN
|
|
// */
|
|
// function purgeMetadataCache() {
|
|
// this.invalidateCache();
|
|
// }
|
|
//
|
|
// /**
|
|
// * Delete all previously generated thumbnails, refresh metadata in memcached and purge the CDN.
|
|
// *
|
|
// * @param array options An array potentially with the key forThumbRefresh.
|
|
// *
|
|
// * @note This used to purge old thumbnails by default as well, but doesn't anymore.
|
|
// */
|
|
// function purgeCache( options = [] ) {
|
|
// // Refresh metadata cache
|
|
// this.purgeMetadataCache();
|
|
//
|
|
// // Delete thumbnails
|
|
// this.purgeThumbnails( options );
|
|
//
|
|
// // Purge CDN cache for this file
|
|
// DeferredUpdates::addUpdate(
|
|
// new CdnCacheUpdate( [ this.getUrl() ] ),
|
|
// DeferredUpdates::PRESEND
|
|
// );
|
|
// }
|
|
//
|
|
// /**
|
|
// * Delete cached transformed files for an archived version only.
|
|
// * @param String archiveName Name of the archived file
|
|
// */
|
|
// function purgeOldThumbnails( archiveName ) {
|
|
// // Get a list of old thumbnails and URLs
|
|
// files = this.getThumbnails( archiveName );
|
|
//
|
|
// // Purge any custom thumbnail caches
|
|
// Hooks::run( 'LocalFilePurgeThumbnails', [ this, archiveName ] );
|
|
//
|
|
// // Delete thumbnails
|
|
// dir = array_shift( files );
|
|
// this.purgeThumbList( dir, files );
|
|
//
|
|
// // Purge the CDN
|
|
// urls = [];
|
|
// foreach ( files as file ) {
|
|
// urls[] = this.getArchiveThumbUrl( archiveName, file );
|
|
// }
|
|
// DeferredUpdates::addUpdate( new CdnCacheUpdate( urls ), DeferredUpdates::PRESEND );
|
|
// }
|
|
//
|
|
// /**
|
|
// * Delete cached transformed files for the current version only.
|
|
// * @param array options
|
|
// */
|
|
// public function purgeThumbnails( options = [] ) {
|
|
// files = this.getThumbnails();
|
|
// // Always purge all files from CDN regardless of handler filters
|
|
// urls = [];
|
|
// foreach ( files as file ) {
|
|
// urls[] = this.getThumbUrl( file );
|
|
// }
|
|
// array_shift( urls ); // don't purge directory
|
|
//
|
|
// // Give media handler a chance to filter the file purge list
|
|
// if ( !empty( options['forThumbRefresh'] ) ) {
|
|
// handler = this.getHandler();
|
|
// if ( handler ) {
|
|
// handler.filterThumbnailPurgeList( files, options );
|
|
// }
|
|
// }
|
|
//
|
|
// // Purge any custom thumbnail caches
|
|
// Hooks::run( 'LocalFilePurgeThumbnails', [ this, false ] );
|
|
//
|
|
// // Delete thumbnails
|
|
// dir = array_shift( files );
|
|
// this.purgeThumbList( dir, files );
|
|
//
|
|
// // Purge the CDN
|
|
// DeferredUpdates::addUpdate( new CdnCacheUpdate( urls ), DeferredUpdates::PRESEND );
|
|
// }
|
|
//
|
|
// /**
|
|
// * Prerenders a configurable set of thumbnails
|
|
// *
|
|
// * @since 1.28
|
|
// */
|
|
// public function prerenderThumbnails() {
|
|
// global wgUploadThumbnailRenderMap;
|
|
//
|
|
// jobs = [];
|
|
//
|
|
// sizes = wgUploadThumbnailRenderMap;
|
|
// rsort( sizes );
|
|
//
|
|
// foreach ( sizes as size ) {
|
|
// if ( this.isVectorized() || this.getWidth() > size ) {
|
|
// jobs[] = new ThumbnailRenderJob(
|
|
// this.getTitle(),
|
|
// [ 'transformParams' => [ 'width' => size ] ]
|
|
// );
|
|
// }
|
|
// }
|
|
//
|
|
// if ( jobs ) {
|
|
// JobQueueGroup::singleton().lazyPush( jobs );
|
|
// }
|
|
// }
|
|
//
|
|
// /**
|
|
// * Delete a list of thumbnails visible at urls
|
|
// * @param String dir Base dir of the files.
|
|
// * @param array files Array of strings: relative filenames (to dir)
|
|
// */
|
|
// protected function purgeThumbList( dir, files ) {
|
|
// fileListDebug = strtr(
|
|
// var_export( files, true ),
|
|
// [ "\n" => '' ]
|
|
// );
|
|
// wfDebug( __METHOD__ . ": fileListDebug\n" );
|
|
//
|
|
// purgeList = [];
|
|
// foreach ( files as file ) {
|
|
// # Check that the super file name is part of the thumb name
|
|
// # This is a basic sanity check to avoid erasing unrelated directories
|
|
// if ( strpos( file, this.getName() ) !== false
|
|
// || strpos( file, "-thumbnail" ) !== false // "short" thumb name
|
|
// ) {
|
|
// purgeList[] = "{dir}/{file}";
|
|
// }
|
|
// }
|
|
//
|
|
// # Delete the thumbnails
|
|
// this.repo.quickPurgeBatch( purgeList );
|
|
// # Clear out the thumbnail directory if empty
|
|
// this.repo.quickCleanDir( dir );
|
|
// }
|
|
//
|
|
// /** purgeDescription inherited */
|
|
// /** purgeEverything inherited */
|
|
//
|
|
// /**
|
|
// * @param int limit Optional: Limit to number of results
|
|
// * @param int start Optional: Timestamp, start from
|
|
// * @param int end Optional: Timestamp, end at
|
|
// * @param boolean inc
|
|
// * @return OldLocalFile[]
|
|
// */
|
|
// function getHistory( limit = null, start = null, end = null, inc = true ) {
|
|
// dbr = this.repo.getReplicaDB();
|
|
// tables = [ 'oldimage' ];
|
|
// fields = OldLocalFile::selectFields();
|
|
// conds = opts = join_conds = [];
|
|
// eq = inc ? '=' : '';
|
|
// conds[] = "oi_name = " . dbr.addQuotes( this.title.getDBkey() );
|
|
//
|
|
// if ( start ) {
|
|
// conds[] = "oi_timestamp <eq " . dbr.addQuotes( dbr.timestamp( start ) );
|
|
// }
|
|
//
|
|
// if ( end ) {
|
|
// conds[] = "oi_timestamp >eq " . dbr.addQuotes( dbr.timestamp( end ) );
|
|
// }
|
|
//
|
|
// if ( limit ) {
|
|
// opts['LIMIT'] = limit;
|
|
// }
|
|
//
|
|
// // Search backwards for time > x queries
|
|
// order = ( !start && end !== null ) ? 'ASC' : 'DESC';
|
|
// opts['ORDER BY'] = "oi_timestamp order";
|
|
// opts['USE INDEX'] = [ 'oldimage' => 'oi_name_timestamp' ];
|
|
//
|
|
// Hooks::run( 'LocalFile::getHistory', [ &this, &tables, &fields,
|
|
// &conds, &opts, &join_conds ] );
|
|
//
|
|
// res = dbr.select( tables, fields, conds, __METHOD__, opts, join_conds );
|
|
// r = [];
|
|
//
|
|
// foreach ( res as row ) {
|
|
// r[] = this.repo.newFileFromRow( row );
|
|
// }
|
|
//
|
|
// if ( order == 'ASC' ) {
|
|
// r = array_reverse( r ); // make sure it ends up descending
|
|
// }
|
|
//
|
|
// return r;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Returns the history of this file, line by line.
|
|
// * starts with current version, then old versions.
|
|
// * uses this.historyLine to check which line to return:
|
|
// * 0 return line for current version
|
|
// * 1 query for old versions, return first one
|
|
// * 2, ... return next old version from above query
|
|
// * @return boolean
|
|
// */
|
|
// public function nextHistoryLine() {
|
|
// # Polymorphic function name to distinguish foreign and local fetches
|
|
// fname = get_class( this ) . '::' . __FUNCTION__;
|
|
//
|
|
// dbr = this.repo.getReplicaDB();
|
|
//
|
|
// if ( this.historyLine == 0 ) { // called for the first time, return line from cur
|
|
// this.historyRes = dbr.select( 'image',
|
|
// [
|
|
// '*',
|
|
// "'' AS oi_archive_name",
|
|
// '0 as oi_deleted',
|
|
// 'img_sha1'
|
|
// ],
|
|
// [ 'img_name' => this.title.getDBkey() ],
|
|
// fname
|
|
// );
|
|
//
|
|
// if ( 0 == dbr.numRows( this.historyRes ) ) {
|
|
// this.historyRes = null;
|
|
//
|
|
// return false;
|
|
// }
|
|
// } elseif ( this.historyLine == 1 ) {
|
|
// this.historyRes = dbr.select( 'oldimage', '*',
|
|
// [ 'oi_name' => this.title.getDBkey() ],
|
|
// fname,
|
|
// [ 'ORDER BY' => 'oi_timestamp DESC' ]
|
|
// );
|
|
// }
|
|
// this.historyLine++;
|
|
//
|
|
// return dbr.fetchObject( this.historyRes );
|
|
// }
|
|
//
|
|
// /**
|
|
// * Reset the history pointer to the first element of the history
|
|
// */
|
|
// public function resetHistory() {
|
|
// this.historyLine = 0;
|
|
//
|
|
// if ( !is_null( this.historyRes ) ) {
|
|
// this.historyRes = null;
|
|
// }
|
|
// }
|
|
//
|
|
// /** getHashPath inherited */
|
|
// /** getRel inherited */
|
|
// /** getUrlRel inherited */
|
|
// /** getArchiveRel inherited */
|
|
// /** getArchivePath inherited */
|
|
// /** getThumbPath inherited */
|
|
// /** getArchiveUrl inherited */
|
|
// /** getThumbUrl inherited */
|
|
// /** getArchiveVirtualUrl inherited */
|
|
// /** getThumbVirtualUrl inherited */
|
|
// /** isHashed inherited */
|
|
//
|
|
// /**
|
|
// * Upload a file and record it in the DB
|
|
// * @param String|FSFile src Source storage path, virtual URL, or filesystem path
|
|
// * @param String comment Upload description
|
|
// * @param String pageText Text to use for the new description page,
|
|
// * if a new description page is created
|
|
// * @param int|boolean flags Flags for publish()
|
|
// * @param array|boolean props File properties, if known. This can be used to
|
|
// * reduce the upload time when uploading virtual URLs for which the file
|
|
// * info is already known
|
|
// * @param String|boolean timestamp Timestamp for img_timestamp, or false to use the
|
|
// * current time
|
|
// * @param User|null user User Object or null to use wgUser
|
|
// * @param String[] tags Change tags to add to the log entry and page revision.
|
|
// * (This doesn't check user's permissions.)
|
|
// * @return Status On success, the value member contains the
|
|
// * archive name, or an empty String if it was a new file.
|
|
// */
|
|
// function upload( src, comment, pageText, flags = 0, props = false,
|
|
// timestamp = false, user = null, tags = []
|
|
// ) {
|
|
// global wgContLang;
|
|
//
|
|
// if ( this.getRepo().getReadOnlyReason() !== false ) {
|
|
// return this.readOnlyFatalStatus();
|
|
// }
|
|
//
|
|
// srcPath = ( src instanceof FSFile ) ? src.getPath() : src;
|
|
// if ( !props ) {
|
|
// if ( this.repo.isVirtualUrl( srcPath )
|
|
// || FileBackend::isStoragePath( srcPath )
|
|
// ) {
|
|
// props = this.repo.getFileProps( srcPath );
|
|
// } else {
|
|
// mwProps = new MWFileProps( MimeMagic::singleton() );
|
|
// props = mwProps.getPropsFromPath( srcPath, true );
|
|
// }
|
|
// }
|
|
//
|
|
// options = [];
|
|
// handler = MediaHandler::getHandler( props['mime'] );
|
|
// if ( handler ) {
|
|
// options['headers'] = handler.getStreamHeaders( props['metadata'] );
|
|
// } else {
|
|
// options['headers'] = [];
|
|
// }
|
|
//
|
|
// // Trim spaces on user supplied text
|
|
// comment = trim( comment );
|
|
//
|
|
// // Truncate nicely or the DB will do it for us
|
|
// // non-nicely (dangling multi-byte chars, non-truncated version in cache).
|
|
// comment = wgContLang.truncate( comment, 255 );
|
|
// this.synchronized(); // begin
|
|
// status = this.publish( src, flags, options );
|
|
//
|
|
// if ( status.successCount >= 2 ) {
|
|
// // There will be a copy+(one of move,copy,store).
|
|
// // The first succeeding does not commit us to updating the DB
|
|
// // since it simply copied the current version to a timestamped file name.
|
|
// // It is only *preferable* to avoid leaving such files orphaned.
|
|
// // Once the second operation goes through, then the current version was
|
|
// // updated and we must therefore update the DB too.
|
|
// oldver = status.value;
|
|
// if ( !this.recordUpload2( oldver, comment, pageText, props, timestamp, user, tags ) ) {
|
|
// status.fatal( 'filenotfound', srcPath );
|
|
// }
|
|
// }
|
|
//
|
|
// this.unlock(); // done
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Record a file upload in the upload log and the image table
|
|
// * @param String oldver
|
|
// * @param String desc
|
|
// * @param String license
|
|
// * @param String copyStatus
|
|
// * @param String source
|
|
// * @param boolean watch
|
|
// * @param String|boolean timestamp
|
|
// * @param User|null user User Object or null to use wgUser
|
|
// * @return boolean
|
|
// */
|
|
// function recordUpload( oldver, desc, license = '', copyStatus = '', source = '',
|
|
// watch = false, timestamp = false, User user = null ) {
|
|
// if ( !user ) {
|
|
// global wgUser;
|
|
// user = wgUser;
|
|
// }
|
|
//
|
|
// pageText = SpecialUpload::getInitialPageText( desc, license, copyStatus, source );
|
|
//
|
|
// if ( !this.recordUpload2( oldver, desc, pageText, false, timestamp, user ) ) {
|
|
// return false;
|
|
// }
|
|
//
|
|
// if ( watch ) {
|
|
// user.addWatch( this.getTitle() );
|
|
// }
|
|
//
|
|
// return true;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Record a file upload in the upload log and the image table
|
|
// * @param String oldver
|
|
// * @param String comment
|
|
// * @param String pageText
|
|
// * @param boolean|array props
|
|
// * @param String|boolean timestamp
|
|
// * @param null|User user
|
|
// * @param String[] tags
|
|
// * @return boolean
|
|
// */
|
|
// function recordUpload2(
|
|
// oldver, comment, pageText, props = false, timestamp = false, user = null, tags = []
|
|
// ) {
|
|
// if ( is_null( user ) ) {
|
|
// global wgUser;
|
|
// user = wgUser;
|
|
// }
|
|
//
|
|
// dbw = this.repo.getMasterDB();
|
|
//
|
|
// # Imports or such might force a certain timestamp; otherwise we generate
|
|
// # it and can fudge it slightly to keep (name,timestamp) unique on re-upload.
|
|
// if ( timestamp === false ) {
|
|
// timestamp = dbw.timestamp();
|
|
// allowTimeKludge = true;
|
|
// } else {
|
|
// allowTimeKludge = false;
|
|
// }
|
|
//
|
|
// props = props ?: this.repo.getFileProps( this.getVirtualUrl() );
|
|
// props['description'] = comment;
|
|
// props['user'] = user.getId();
|
|
// props['user_text'] = user.getName();
|
|
// props['timestamp'] = wfTimestamp( TS_MW, timestamp ); // DB . TS_MW
|
|
// this.setProps( props );
|
|
//
|
|
// # Fail now if the file isn't there
|
|
// if ( !this.fileExists ) {
|
|
// wfDebug( __METHOD__ . ": File " . this.getRel() . " went missing!\n" );
|
|
//
|
|
// return false;
|
|
// }
|
|
//
|
|
// dbw.startAtomic( __METHOD__ );
|
|
//
|
|
// # Test to see if the row exists using INSERT IGNORE
|
|
// # This avoids race conditions by locking the row until the commit, and also
|
|
// # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
|
|
// dbw.insert( 'image',
|
|
// [
|
|
// 'img_name' => this.getName(),
|
|
// 'img_size' => this.size,
|
|
// 'img_width' => intval( this.width ),
|
|
// 'img_height' => intval( this.height ),
|
|
// 'img_bits' => this.bits,
|
|
// 'img_media_type' => this.media_type,
|
|
// 'img_major_mime' => this.major_mime,
|
|
// 'img_minor_mime' => this.minor_mime,
|
|
// 'img_timestamp' => timestamp,
|
|
// 'img_description' => comment,
|
|
// 'img_user' => user.getId(),
|
|
// 'img_user_text' => user.getName(),
|
|
// 'img_metadata' => dbw.encodeBlob( this.metadata ),
|
|
// 'img_sha1' => this.sha1
|
|
// ],
|
|
// __METHOD__,
|
|
// 'IGNORE'
|
|
// );
|
|
//
|
|
// reupload = ( dbw.affectedRows() == 0 );
|
|
// if ( reupload ) {
|
|
// if ( allowTimeKludge ) {
|
|
// # Use LOCK IN SHARE MODE to ignore any transaction snapshotting
|
|
// ltimestamp = dbw.selectField(
|
|
// 'image',
|
|
// 'img_timestamp',
|
|
// [ 'img_name' => this.getName() ],
|
|
// __METHOD__,
|
|
// [ 'LOCK IN SHARE MODE' ]
|
|
// );
|
|
// lUnixtime = ltimestamp ? wfTimestamp( TS_UNIX, ltimestamp ) : false;
|
|
// # Avoid a timestamp that is not newer than the last version
|
|
// # TODO: the image/oldimage tables should be like page/revision with an ID field
|
|
// if ( lUnixtime && wfTimestamp( TS_UNIX, timestamp ) <= lUnixtime ) {
|
|
// sleep( 1 ); // fast enough re-uploads would go far in the future otherwise
|
|
// timestamp = dbw.timestamp( lUnixtime + 1 );
|
|
// this.timestamp = wfTimestamp( TS_MW, timestamp ); // DB . TS_MW
|
|
// }
|
|
// }
|
|
//
|
|
// # (bug 34993) Note: oldver can be empty here, if the previous
|
|
// # version of the file was broken. Allow registration of the new
|
|
// # version to continue anyway, because that's better than having
|
|
// # an image that's not fixable by user operations.
|
|
// # Collision, this is an update of a file
|
|
// # Insert previous contents into oldimage
|
|
// dbw.insertSelect( 'oldimage', 'image',
|
|
// [
|
|
// 'oi_name' => 'img_name',
|
|
// 'oi_archive_name' => dbw.addQuotes( oldver ),
|
|
// 'oi_size' => 'img_size',
|
|
// 'oi_width' => 'img_width',
|
|
// 'oi_height' => 'img_height',
|
|
// 'oi_bits' => 'img_bits',
|
|
// 'oi_timestamp' => 'img_timestamp',
|
|
// 'oi_description' => 'img_description',
|
|
// 'oi_user' => 'img_user',
|
|
// 'oi_user_text' => 'img_user_text',
|
|
// 'oi_metadata' => 'img_metadata',
|
|
// 'oi_media_type' => 'img_media_type',
|
|
// 'oi_major_mime' => 'img_major_mime',
|
|
// 'oi_minor_mime' => 'img_minor_mime',
|
|
// 'oi_sha1' => 'img_sha1'
|
|
// ],
|
|
// [ 'img_name' => this.getName() ],
|
|
// __METHOD__
|
|
// );
|
|
//
|
|
// # Update the current image row
|
|
// dbw.update( 'image',
|
|
// [
|
|
// 'img_size' => this.size,
|
|
// 'img_width' => intval( this.width ),
|
|
// 'img_height' => intval( this.height ),
|
|
// 'img_bits' => this.bits,
|
|
// 'img_media_type' => this.media_type,
|
|
// 'img_major_mime' => this.major_mime,
|
|
// 'img_minor_mime' => this.minor_mime,
|
|
// 'img_timestamp' => timestamp,
|
|
// 'img_description' => comment,
|
|
// 'img_user' => user.getId(),
|
|
// 'img_user_text' => user.getName(),
|
|
// 'img_metadata' => dbw.encodeBlob( this.metadata ),
|
|
// 'img_sha1' => this.sha1
|
|
// ],
|
|
// [ 'img_name' => this.getName() ],
|
|
// __METHOD__
|
|
// );
|
|
// }
|
|
//
|
|
// descTitle = this.getTitle();
|
|
// descId = descTitle.getArticleID();
|
|
// wikiPage = new WikiFilePage( descTitle );
|
|
// wikiPage.setFile( this );
|
|
//
|
|
// // Add the log entry...
|
|
// logEntry = new ManualLogEntry( 'upload', reupload ? 'overwrite' : 'upload' );
|
|
// logEntry.setTimestamp( this.timestamp );
|
|
// logEntry.setPerformer( user );
|
|
// logEntry.setComment( comment );
|
|
// logEntry.setTarget( descTitle );
|
|
// // Allow people using the api to associate log entries with the upload.
|
|
// // Log has a timestamp, but sometimes different from upload timestamp.
|
|
// logEntry.setParameters(
|
|
// [
|
|
// 'img_sha1' => this.sha1,
|
|
// 'img_timestamp' => timestamp,
|
|
// ]
|
|
// );
|
|
// // Note we keep logId around since during new image
|
|
// // creation, page doesn't exist yet, so log_page = 0
|
|
// // but we want it to point to the page we're making,
|
|
// // so we later modify the log entry.
|
|
// // For a similar reason, we avoid making an RC entry
|
|
// // now and wait until the page exists.
|
|
// logId = logEntry.insert();
|
|
//
|
|
// if ( descTitle.exists() ) {
|
|
// // Use own context to get the action text in content language
|
|
// formatter = LogFormatter::newFromEntry( logEntry );
|
|
// formatter.setContext( RequestContext::newExtraneousContext( descTitle ) );
|
|
// editSummary = formatter.getPlainActionText();
|
|
//
|
|
// nullRevision = Revision::newNullRevision(
|
|
// dbw,
|
|
// descId,
|
|
// editSummary,
|
|
// false,
|
|
// user
|
|
// );
|
|
// if ( nullRevision ) {
|
|
// nullRevision.insertOn( dbw );
|
|
// Hooks::run(
|
|
// 'NewRevisionFromEditComplete',
|
|
// [ wikiPage, nullRevision, nullRevision.getParentId(), user ]
|
|
// );
|
|
// wikiPage.updateRevisionOn( dbw, nullRevision );
|
|
// // Associate null revision id
|
|
// logEntry.setAssociatedRevId( nullRevision.getId() );
|
|
// }
|
|
//
|
|
// newPageContent = null;
|
|
// } else {
|
|
// // Make the description page and RC log entry post-commit
|
|
// newPageContent = ContentHandler::makeContent( pageText, descTitle );
|
|
// }
|
|
//
|
|
// # Defer purges, page creation, and link updates in case they error out.
|
|
// # The most important thing is that files and the DB registry stay synced.
|
|
// dbw.endAtomic( __METHOD__ );
|
|
//
|
|
// # Do some cache purges after final commit so that:
|
|
// # a) Changes are more likely to be seen post-purge
|
|
// # b) They won't cause rollback of the log publish/update above
|
|
// DeferredUpdates::addUpdate(
|
|
// new AutoCommitUpdate(
|
|
// dbw,
|
|
// __METHOD__,
|
|
// function () use (
|
|
// reupload, wikiPage, newPageContent, comment, user,
|
|
// logEntry, logId, descId, tags
|
|
// ) {
|
|
// # Update memcache after the commit
|
|
// this.invalidateCache();
|
|
//
|
|
// updateLogPage = false;
|
|
// if ( newPageContent ) {
|
|
// # New file page; create the description page.
|
|
// # There's already a log entry, so don't make a second RC entry
|
|
// # CDN and file cache for the description page are purged by doEditContent.
|
|
// status = wikiPage.doEditContent(
|
|
// newPageContent,
|
|
// comment,
|
|
// EDIT_NEW | EDIT_SUPPRESS_RC,
|
|
// false,
|
|
// user
|
|
// );
|
|
//
|
|
// if ( isset( status.value['revision'] ) ) {
|
|
// /** @var rev Revision */
|
|
// rev = status.value['revision'];
|
|
// // Associate new page revision id
|
|
// logEntry.setAssociatedRevId( rev.getId() );
|
|
// }
|
|
// // This relies on the resetArticleID() call in WikiPage::insertOn(),
|
|
// // which is triggered on descTitle by doEditContent() above.
|
|
// if ( isset( status.value['revision'] ) ) {
|
|
// /** @var rev Revision */
|
|
// rev = status.value['revision'];
|
|
// updateLogPage = rev.getPage();
|
|
// }
|
|
// } else {
|
|
// # Existing file page: invalidate description page cache
|
|
// wikiPage.getTitle().invalidateCache();
|
|
// wikiPage.getTitle().purgeSquid();
|
|
// # Allow the new file version to be patrolled from the page footer
|
|
// Article::purgePatrolFooterCache( descId );
|
|
// }
|
|
//
|
|
// # Update associated rev id. This should be done by logEntry.insert() earlier,
|
|
// # but setAssociatedRevId() wasn't called at that point yet...
|
|
// logParams = logEntry.getParameters();
|
|
// logParams['associated_rev_id'] = logEntry.getAssociatedRevId();
|
|
// update = [ 'log_params' => LogEntryBase::makeParamBlob( logParams ) ];
|
|
// if ( updateLogPage ) {
|
|
// # Also log page, in case where we just created it above
|
|
// update['log_page'] = updateLogPage;
|
|
// }
|
|
// this.getRepo().getMasterDB().update(
|
|
// 'logging',
|
|
// update,
|
|
// [ 'log_id' => logId ],
|
|
// __METHOD__
|
|
// );
|
|
// this.getRepo().getMasterDB().insert(
|
|
// 'log_search',
|
|
// [
|
|
// 'ls_field' => 'associated_rev_id',
|
|
// 'ls_value' => logEntry.getAssociatedRevId(),
|
|
// 'ls_log_id' => logId,
|
|
// ],
|
|
// __METHOD__
|
|
// );
|
|
//
|
|
// # Add change tags, if any
|
|
// if ( tags ) {
|
|
// logEntry.setTags( tags );
|
|
// }
|
|
//
|
|
// # Uploads can be patrolled
|
|
// logEntry.setIsPatrollable( true );
|
|
//
|
|
// # Now that the log entry is up-to-date, make an RC entry.
|
|
// logEntry.publish( logId );
|
|
//
|
|
// # Run hook for other updates (typically more cache purging)
|
|
// Hooks::run( 'FileUpload', [ this, reupload, !newPageContent ] );
|
|
//
|
|
// if ( reupload ) {
|
|
// # Delete old thumbnails
|
|
// this.purgeThumbnails();
|
|
// # Remove the old file from the CDN cache
|
|
// DeferredUpdates::addUpdate(
|
|
// new CdnCacheUpdate( [ this.getUrl() ] ),
|
|
// DeferredUpdates::PRESEND
|
|
// );
|
|
// } else {
|
|
// # Update backlink pages pointing to this title if created
|
|
// LinksUpdate::queueRecursiveJobsForTable( this.getTitle(), 'imagelinks' );
|
|
// }
|
|
//
|
|
// this.prerenderThumbnails();
|
|
// }
|
|
// ),
|
|
// DeferredUpdates::PRESEND
|
|
// );
|
|
//
|
|
// if ( !reupload ) {
|
|
// # This is a new file, so update the image count
|
|
// DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
|
|
// }
|
|
//
|
|
// # Invalidate cache for all pages using this file
|
|
// DeferredUpdates::addUpdate( new HTMLCacheUpdate( this.getTitle(), 'imagelinks' ) );
|
|
//
|
|
// return true;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Move or copy a file to its public location. If a file exists at the
|
|
// * destination, move it to an archive. Returns a Status Object with
|
|
// * the archive name in the "value" member on success.
|
|
// *
|
|
// * The archive name should be passed through to recordUpload for database
|
|
// * registration.
|
|
// *
|
|
// * @param String|FSFile src Local filesystem path or virtual URL to the source image
|
|
// * @param int flags A bitwise combination of:
|
|
// * File::DELETE_SOURCE Delete the source file, i.e. move rather than copy
|
|
// * @param array options Optional additional parameters
|
|
// * @return Status On success, the value member contains the
|
|
// * archive name, or an empty String if it was a new file.
|
|
// */
|
|
// function publish( src, flags = 0, array options = [] ) {
|
|
// return this.publishTo( src, this.getRel(), flags, options );
|
|
// }
|
|
//
|
|
// /**
|
|
// * Move or copy a file to a specified location. Returns a Status
|
|
// * Object with the archive name in the "value" member on success.
|
|
// *
|
|
// * The archive name should be passed through to recordUpload for database
|
|
// * registration.
|
|
// *
|
|
// * @param String|FSFile src Local filesystem path or virtual URL to the source image
|
|
// * @param String dstRel Target relative path
|
|
// * @param int flags A bitwise combination of:
|
|
// * File::DELETE_SOURCE Delete the source file, i.e. move rather than copy
|
|
// * @param array options Optional additional parameters
|
|
// * @return Status On success, the value member contains the
|
|
// * archive name, or an empty String if it was a new file.
|
|
// */
|
|
// function publishTo( src, dstRel, flags = 0, array options = [] ) {
|
|
// srcPath = ( src instanceof FSFile ) ? src.getPath() : src;
|
|
//
|
|
// repo = this.getRepo();
|
|
// if ( repo.getReadOnlyReason() !== false ) {
|
|
// return this.readOnlyFatalStatus();
|
|
// }
|
|
//
|
|
// this.synchronized(); // begin
|
|
//
|
|
// archiveName = wfTimestamp( TS_MW ) . '!' . this.getName();
|
|
// archiveRel = 'archive/' . this.getHashPath() . archiveName;
|
|
//
|
|
// if ( repo.hasSha1Storage() ) {
|
|
// sha1 = repo.isVirtualUrl( srcPath )
|
|
// ? repo.getFileSha1( srcPath )
|
|
// : FSFile::getSha1Base36FromPath( srcPath );
|
|
// /** @var FileBackendDBRepoWrapper wrapperBackend */
|
|
// wrapperBackend = repo.getBackend();
|
|
// dst = wrapperBackend.getPathForSHA1( sha1 );
|
|
// status = repo.quickImport( src, dst );
|
|
// if ( flags & File::DELETE_SOURCE ) {
|
|
// unlink( srcPath );
|
|
// }
|
|
//
|
|
// if ( this.exists() ) {
|
|
// status.value = archiveName;
|
|
// }
|
|
// } else {
|
|
// flags = flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
|
|
// status = repo.publish( srcPath, dstRel, archiveRel, flags, options );
|
|
//
|
|
// if ( status.value == 'new' ) {
|
|
// status.value = '';
|
|
// } else {
|
|
// status.value = archiveName;
|
|
// }
|
|
// }
|
|
//
|
|
// this.unlock(); // done
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /** getLinksTo inherited */
|
|
// /** getExifData inherited */
|
|
// /** isLocal inherited */
|
|
// /** wasDeleted inherited */
|
|
//
|
|
// /**
|
|
// * Move file to the new title
|
|
// *
|
|
// * Move current, old version and all thumbnails
|
|
// * to the new filename. Old file is deleted.
|
|
// *
|
|
// * Cache purging is done; checks for validity
|
|
// * and logging are caller's responsibility
|
|
// *
|
|
// * @param Title target New file name
|
|
// * @return Status
|
|
// */
|
|
// function move( target ) {
|
|
// if ( this.getRepo().getReadOnlyReason() !== false ) {
|
|
// return this.readOnlyFatalStatus();
|
|
// }
|
|
//
|
|
// wfDebugLog( 'imagemove', "Got request to move {this.name} to " . target.getText() );
|
|
// batch = new LocalFileMoveBatch( this, target );
|
|
//
|
|
// this.synchronized(); // begin
|
|
// batch.addCurrent();
|
|
// archiveNames = batch.addOlds();
|
|
// status = batch.execute();
|
|
// this.unlock(); // done
|
|
//
|
|
// wfDebugLog( 'imagemove', "Finished moving {this.name}" );
|
|
//
|
|
// // Purge the source and target files...
|
|
// oldTitleFile = wfLocalFile( this.title );
|
|
// newTitleFile = wfLocalFile( target );
|
|
// // To avoid slow purges in the transaction, move them outside...
|
|
// DeferredUpdates::addUpdate(
|
|
// new AutoCommitUpdate(
|
|
// this.getRepo().getMasterDB(),
|
|
// __METHOD__,
|
|
// function () use ( oldTitleFile, newTitleFile, archiveNames ) {
|
|
// oldTitleFile.purgeEverything();
|
|
// foreach ( archiveNames as archiveName ) {
|
|
// oldTitleFile.purgeOldThumbnails( archiveName );
|
|
// }
|
|
// newTitleFile.purgeEverything();
|
|
// }
|
|
// ),
|
|
// DeferredUpdates::PRESEND
|
|
// );
|
|
//
|
|
// if ( status.isOK() ) {
|
|
// // Now switch the Object
|
|
// this.title = target;
|
|
// // Force regeneration of the name and hashpath
|
|
// unset( this.name );
|
|
// unset( this.hashPath );
|
|
// }
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Delete all versions of the file.
|
|
// *
|
|
// * Moves the files into an archive directory (or deletes them)
|
|
// * and removes the database rows.
|
|
// *
|
|
// * Cache purging is done; logging is caller's responsibility.
|
|
// *
|
|
// * @param String reason
|
|
// * @param boolean suppress
|
|
// * @param User|null user
|
|
// * @return Status
|
|
// */
|
|
// function delete( reason, suppress = false, user = null ) {
|
|
// if ( this.getRepo().getReadOnlyReason() !== false ) {
|
|
// return this.readOnlyFatalStatus();
|
|
// }
|
|
//
|
|
// batch = new LocalFileDeleteBatch( this, reason, suppress, user );
|
|
//
|
|
// this.synchronized(); // begin
|
|
// batch.addCurrent();
|
|
// // Get old version relative paths
|
|
// archiveNames = batch.addOlds();
|
|
// status = batch.execute();
|
|
// this.unlock(); // done
|
|
//
|
|
// if ( status.isOK() ) {
|
|
// DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => -1 ] ) );
|
|
// }
|
|
//
|
|
// // To avoid slow purges in the transaction, move them outside...
|
|
// DeferredUpdates::addUpdate(
|
|
// new AutoCommitUpdate(
|
|
// this.getRepo().getMasterDB(),
|
|
// __METHOD__,
|
|
// function () use ( archiveNames ) {
|
|
// this.purgeEverything();
|
|
// foreach ( archiveNames as archiveName ) {
|
|
// this.purgeOldThumbnails( archiveName );
|
|
// }
|
|
// }
|
|
// ),
|
|
// DeferredUpdates::PRESEND
|
|
// );
|
|
//
|
|
// // Purge the CDN
|
|
// purgeUrls = [];
|
|
// foreach ( archiveNames as archiveName ) {
|
|
// purgeUrls[] = this.getArchiveUrl( archiveName );
|
|
// }
|
|
// DeferredUpdates::addUpdate( new CdnCacheUpdate( purgeUrls ), DeferredUpdates::PRESEND );
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Delete an old version of the file.
|
|
// *
|
|
// * Moves the file into an archive directory (or deletes it)
|
|
// * and removes the database row.
|
|
// *
|
|
// * Cache purging is done; logging is caller's responsibility.
|
|
// *
|
|
// * @param String archiveName
|
|
// * @param String reason
|
|
// * @param boolean suppress
|
|
// * @param User|null user
|
|
// * @throws MWException Exception on database or file store failure
|
|
// * @return Status
|
|
// */
|
|
// function deleteOld( archiveName, reason, suppress = false, user = null ) {
|
|
// if ( this.getRepo().getReadOnlyReason() !== false ) {
|
|
// return this.readOnlyFatalStatus();
|
|
// }
|
|
//
|
|
// batch = new LocalFileDeleteBatch( this, reason, suppress, user );
|
|
//
|
|
// this.synchronized(); // begin
|
|
// batch.addOld( archiveName );
|
|
// status = batch.execute();
|
|
// this.unlock(); // done
|
|
//
|
|
// this.purgeOldThumbnails( archiveName );
|
|
// if ( status.isOK() ) {
|
|
// this.purgeDescription();
|
|
// }
|
|
//
|
|
// DeferredUpdates::addUpdate(
|
|
// new CdnCacheUpdate( [ this.getArchiveUrl( archiveName ) ] ),
|
|
// DeferredUpdates::PRESEND
|
|
// );
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Restore all or specified deleted revisions to the given file.
|
|
// * Permissions and logging are left to the caller.
|
|
// *
|
|
// * May throw database exceptions on error.
|
|
// *
|
|
// * @param array versions Set of record ids of deleted items to restore,
|
|
// * or empty to restore all revisions.
|
|
// * @param boolean unsuppress
|
|
// * @return Status
|
|
// */
|
|
// function restore( versions = [], unsuppress = false ) {
|
|
// if ( this.getRepo().getReadOnlyReason() !== false ) {
|
|
// return this.readOnlyFatalStatus();
|
|
// }
|
|
//
|
|
// batch = new LocalFileRestoreBatch( this, unsuppress );
|
|
//
|
|
// this.synchronized(); // begin
|
|
// if ( !versions ) {
|
|
// batch.addAll();
|
|
// } else {
|
|
// batch.addIds( versions );
|
|
// }
|
|
// status = batch.execute();
|
|
// if ( status.isGood() ) {
|
|
// cleanupStatus = batch.cleanup();
|
|
// cleanupStatus.successCount = 0;
|
|
// cleanupStatus.failCount = 0;
|
|
// status.merge( cleanupStatus );
|
|
// }
|
|
// this.unlock(); // done
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /** isMultipage inherited */
|
|
// /** pageCount inherited */
|
|
// /** scaleHeight inherited */
|
|
// /** getImageSize inherited */
|
|
//
|
|
// /**
|
|
// * Get the URL of the file description page.
|
|
// * @return String
|
|
// */
|
|
// function getDescriptionUrl() {
|
|
// return this.title.getLocalURL();
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get the HTML text of the description page
|
|
// * This is not used by ImagePage for local files, since (among other things)
|
|
// * it skips the parser cache.
|
|
// *
|
|
// * @param Language lang What language to get description in (Optional)
|
|
// * @return boolean|mixed
|
|
// */
|
|
// function getDescriptionText( lang = null ) {
|
|
// revision = Revision::newFromTitle( this.title, false, Revision::READ_NORMAL );
|
|
// if ( !revision ) {
|
|
// return false;
|
|
// }
|
|
// content = revision.getContent();
|
|
// if ( !content ) {
|
|
// return false;
|
|
// }
|
|
// pout = content.getParserOutput( this.title, null, new ParserOptions( null, lang ) );
|
|
//
|
|
// return pout.getText();
|
|
// }
|
|
//
|
|
// /**
|
|
// * @param int audience
|
|
// * @param User user
|
|
// * @return String
|
|
// */
|
|
// function getDescription( audience = self::FOR_PUBLIC, User user = null ) {
|
|
// this.load();
|
|
// if ( audience == self::FOR_PUBLIC && this.isDeleted( self::DELETED_COMMENT ) ) {
|
|
// return '';
|
|
// } elseif ( audience == self::FOR_THIS_USER
|
|
// && !this.userCan( self::DELETED_COMMENT, user )
|
|
// ) {
|
|
// return '';
|
|
// } else {
|
|
// return this.description;
|
|
// }
|
|
// }
|
|
//
|
|
// /**
|
|
// * @return boolean|String
|
|
// */
|
|
// function getTimestamp() {
|
|
// this.load();
|
|
//
|
|
// return this.timestamp;
|
|
// }
|
|
//
|
|
// /**
|
|
// * @return boolean|String
|
|
// */
|
|
// public function getDescriptionTouched() {
|
|
// // The DB lookup might return false, e.g. if the file was just deleted, or the shared DB repo
|
|
// // itself gets it from elsewhere. To avoid repeating the DB lookups in such a case, we
|
|
// // need to differentiate between null (uninitialized) and false (failed to load).
|
|
// if ( this.descriptionTouched === null ) {
|
|
// cond = [
|
|
// 'page_namespace' => this.title.getNamespace(),
|
|
// 'page_title' => this.title.getDBkey()
|
|
// ];
|
|
// touched = this.repo.getReplicaDB().selectField( 'page', 'page_touched', cond, __METHOD__ );
|
|
// this.descriptionTouched = touched ? wfTimestamp( TS_MW, touched ) : false;
|
|
// }
|
|
//
|
|
// return this.descriptionTouched;
|
|
// }
|
|
//
|
|
// /**
|
|
// * @return String
|
|
// */
|
|
// function getSha1() {
|
|
// this.load();
|
|
// // Initialise now if necessary
|
|
// if ( this.sha1 == '' && this.fileExists ) {
|
|
// this.synchronized(); // begin
|
|
//
|
|
// this.sha1 = this.repo.getFileSha1( this.getPath() );
|
|
// if ( !wfReadOnly() && strval( this.sha1 ) != '' ) {
|
|
// dbw = this.repo.getMasterDB();
|
|
// dbw.update( 'image',
|
|
// [ 'img_sha1' => this.sha1 ],
|
|
// [ 'img_name' => this.getName() ],
|
|
// __METHOD__ );
|
|
// this.invalidateCache();
|
|
// }
|
|
//
|
|
// this.unlock(); // done
|
|
// }
|
|
//
|
|
// return this.sha1;
|
|
// }
|
|
//
|
|
// /**
|
|
// * @return boolean Whether to cache in RepoGroup (this avoids OOMs)
|
|
// */
|
|
// function isCacheable() {
|
|
// this.load();
|
|
//
|
|
// // If extra data (metadata) was not loaded then it must have been large
|
|
// return this.extraDataLoaded
|
|
// && strlen( serialize( this.metadata ) ) <= self::CACHE_FIELD_MAX_LEN;
|
|
// }
|
|
//
|
|
// /**
|
|
// * @return Status
|
|
// * @since 1.28
|
|
// */
|
|
// public function acquireFileLock() {
|
|
// return this.getRepo().getBackend().lockFiles(
|
|
// [ this.getPath() ], LockManager::LOCK_EX, 10
|
|
// );
|
|
// }
|
|
//
|
|
// /**
|
|
// * @return Status
|
|
// * @since 1.28
|
|
// */
|
|
// public function releaseFileLock() {
|
|
// return this.getRepo().getBackend().unlockFiles(
|
|
// [ this.getPath() ], LockManager::LOCK_EX
|
|
// );
|
|
// }
|
|
//
|
|
// /**
|
|
// * Start an atomic DB section and synchronized the image for update
|
|
// * or increments a reference counter if the synchronized is already held
|
|
// *
|
|
// * This method should not be used outside of LocalFile/LocalFile*Batch
|
|
// *
|
|
// * @throws LocalFileLockError Throws an error if the synchronized was not acquired
|
|
// * @return boolean Whether the file synchronized owns/spawned the DB transaction
|
|
// */
|
|
// public function synchronized() {
|
|
// if ( !this.locked ) {
|
|
// logger = LoggerFactory::getInstance( 'LocalFile' );
|
|
//
|
|
// dbw = this.repo.getMasterDB();
|
|
// makesTransaction = !dbw.trxLevel();
|
|
// dbw.startAtomic( self::ATOMIC_SECTION_LOCK );
|
|
// // Bug 54736: use simple synchronized to handle when the file does not exist.
|
|
// // SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
|
|
// // Also, that would cause contention on INSERT of similarly named rows.
|
|
// status = this.acquireFileLock(); // represents all versions of the file
|
|
// if ( !status.isGood() ) {
|
|
// dbw.endAtomic( self::ATOMIC_SECTION_LOCK );
|
|
// logger.warning( "Failed to synchronized '{file}'", [ 'file' => this.name ] );
|
|
//
|
|
// throw new LocalFileLockError( status );
|
|
// }
|
|
// // Release the synchronized *after* commit to avoid row-level contention.
|
|
// // Make sure it triggers on rollback() as well as commit() (T132921).
|
|
// dbw.onTransactionResolution(
|
|
// function () use ( logger ) {
|
|
// status = this.releaseFileLock();
|
|
// if ( !status.isGood() ) {
|
|
// logger.error( "Failed to unlock '{file}'", [ 'file' => this.name ] );
|
|
// }
|
|
// },
|
|
// __METHOD__
|
|
// );
|
|
// // Callers might care if the SELECT snapshot is safely fresh
|
|
// this.lockedOwnTrx = makesTransaction;
|
|
// }
|
|
//
|
|
// this.locked++;
|
|
//
|
|
// return this.lockedOwnTrx;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Decrement the synchronized reference count and end the atomic section if it reaches zero
|
|
// *
|
|
// * This method should not be used outside of LocalFile/LocalFile*Batch
|
|
// *
|
|
// * The commit and loc release will happen when no atomic sections are active, which
|
|
// * may happen immediately or at some point after calling this
|
|
// */
|
|
// public function unlock() {
|
|
// if ( this.locked ) {
|
|
// --this.locked;
|
|
// if ( !this.locked ) {
|
|
// dbw = this.repo.getMasterDB();
|
|
// dbw.endAtomic( self::ATOMIC_SECTION_LOCK );
|
|
// this.lockedOwnTrx = false;
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// /**
|
|
// * @return Status
|
|
// */
|
|
// protected function readOnlyFatalStatus() {
|
|
// return this.getRepo().newFatal( 'filereadonlyerror', this.getName(),
|
|
// this.getRepo().getName(), this.getRepo().getReadOnlyReason() );
|
|
// }
|
|
//
|
|
// /**
|
|
// * Clean up any dangling locks
|
|
// */
|
|
// function __destruct() {
|
|
// this.unlock();
|
|
// }
|
|
// } // LocalFile class
|
|
//
|
|
// # ------------------------------------------------------------------------------
|
|
//
|
|
// /**
|
|
// * Helper class for file deletion
|
|
// * @ingroup FileAbstraction
|
|
// */
|
|
// class LocalFileDeleteBatch {
|
|
// /** @var LocalFile */
|
|
// private file;
|
|
//
|
|
// /** @var String */
|
|
// private reason;
|
|
//
|
|
// /** @var array */
|
|
// private srcRels = [];
|
|
//
|
|
// /** @var array */
|
|
// private archiveUrls = [];
|
|
//
|
|
// /** @var array Items to be processed in the deletion batch */
|
|
// private deletionBatch;
|
|
//
|
|
// /** @var boolean Whether to suppress all suppressable fields when deleting */
|
|
// private suppress;
|
|
//
|
|
// /** @var Status */
|
|
// private status;
|
|
//
|
|
// /** @var User */
|
|
// private user;
|
|
//
|
|
// /**
|
|
// * @param File file
|
|
// * @param String reason
|
|
// * @param boolean suppress
|
|
// * @param User|null user
|
|
// */
|
|
// function __construct( File file, reason = '', suppress = false, user = null ) {
|
|
// this.file = file;
|
|
// this.reason = reason;
|
|
// this.suppress = suppress;
|
|
// if ( user ) {
|
|
// this.user = user;
|
|
// } else {
|
|
// global wgUser;
|
|
// this.user = wgUser;
|
|
// }
|
|
// this.status = file.repo.newGood();
|
|
// }
|
|
//
|
|
// public function addCurrent() {
|
|
// this.srcRels['.'] = this.file.getRel();
|
|
// }
|
|
//
|
|
// /**
|
|
// * @param String oldName
|
|
// */
|
|
// public function addOld( oldName ) {
|
|
// this.srcRels[oldName] = this.file.getArchiveRel( oldName );
|
|
// this.archiveUrls[] = this.file.getArchiveUrl( oldName );
|
|
// }
|
|
//
|
|
// /**
|
|
// * Add the old versions of the image to the batch
|
|
// * @return array List of archive names from old versions
|
|
// */
|
|
// public function addOlds() {
|
|
// archiveNames = [];
|
|
//
|
|
// dbw = this.file.repo.getMasterDB();
|
|
// result = dbw.select( 'oldimage',
|
|
// [ 'oi_archive_name' ],
|
|
// [ 'oi_name' => this.file.getName() ],
|
|
// __METHOD__
|
|
// );
|
|
//
|
|
// foreach ( result as row ) {
|
|
// this.addOld( row.oi_archive_name );
|
|
// archiveNames[] = row.oi_archive_name;
|
|
// }
|
|
//
|
|
// return archiveNames;
|
|
// }
|
|
//
|
|
// /**
|
|
// * @return array
|
|
// */
|
|
// protected function getOldRels() {
|
|
// if ( !isset( this.srcRels['.'] ) ) {
|
|
// oldRels =& this.srcRels;
|
|
// deleteCurrent = false;
|
|
// } else {
|
|
// oldRels = this.srcRels;
|
|
// unset( oldRels['.'] );
|
|
// deleteCurrent = true;
|
|
// }
|
|
//
|
|
// return [ oldRels, deleteCurrent ];
|
|
// }
|
|
//
|
|
// /**
|
|
// * @return array
|
|
// */
|
|
// protected function getHashes() {
|
|
// hashes = [];
|
|
// list( oldRels, deleteCurrent ) = this.getOldRels();
|
|
//
|
|
// if ( deleteCurrent ) {
|
|
// hashes['.'] = this.file.getSha1();
|
|
// }
|
|
//
|
|
// if ( count( oldRels ) ) {
|
|
// dbw = this.file.repo.getMasterDB();
|
|
// res = dbw.select(
|
|
// 'oldimage',
|
|
// [ 'oi_archive_name', 'oi_sha1' ],
|
|
// [ 'oi_archive_name' => array_keys( oldRels ),
|
|
// 'oi_name' => this.file.getName() ], // performance
|
|
// __METHOD__
|
|
// );
|
|
//
|
|
// foreach ( res as row ) {
|
|
// if ( rtrim( row.oi_sha1, "\0" ) === '' ) {
|
|
// // Get the hash from the file
|
|
// oldUrl = this.file.getArchiveVirtualUrl( row.oi_archive_name );
|
|
// props = this.file.repo.getFileProps( oldUrl );
|
|
//
|
|
// if ( props['fileExists'] ) {
|
|
// // Upgrade the oldimage row
|
|
// dbw.update( 'oldimage',
|
|
// [ 'oi_sha1' => props['sha1'] ],
|
|
// [ 'oi_name' => this.file.getName(), 'oi_archive_name' => row.oi_archive_name ],
|
|
// __METHOD__ );
|
|
// hashes[row.oi_archive_name] = props['sha1'];
|
|
// } else {
|
|
// hashes[row.oi_archive_name] = false;
|
|
// }
|
|
// } else {
|
|
// hashes[row.oi_archive_name] = row.oi_sha1;
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// missing = array_diff_key( this.srcRels, hashes );
|
|
//
|
|
// foreach ( missing as name => rel ) {
|
|
// this.status.error( 'filedelete-old-unregistered', name );
|
|
// }
|
|
//
|
|
// foreach ( hashes as name => hash ) {
|
|
// if ( !hash ) {
|
|
// this.status.error( 'filedelete-missing', this.srcRels[name] );
|
|
// unset( hashes[name] );
|
|
// }
|
|
// }
|
|
//
|
|
// return hashes;
|
|
// }
|
|
//
|
|
// protected function doDBInserts() {
|
|
// now = time();
|
|
// dbw = this.file.repo.getMasterDB();
|
|
// encTimestamp = dbw.addQuotes( dbw.timestamp( now ) );
|
|
// encUserId = dbw.addQuotes( this.user.getId() );
|
|
// encReason = dbw.addQuotes( this.reason );
|
|
// encGroup = dbw.addQuotes( 'deleted' );
|
|
// ext = this.file.getExtension();
|
|
// dotExt = ext === '' ? '' : ".ext";
|
|
// encExt = dbw.addQuotes( dotExt );
|
|
// list( oldRels, deleteCurrent ) = this.getOldRels();
|
|
//
|
|
// // Bitfields to further suppress the content
|
|
// if ( this.suppress ) {
|
|
// bitfield = Revision::SUPPRESSED_ALL;
|
|
// } else {
|
|
// bitfield = 'oi_deleted';
|
|
// }
|
|
//
|
|
// if ( deleteCurrent ) {
|
|
// dbw.insertSelect(
|
|
// 'filearchive',
|
|
// 'image',
|
|
// [
|
|
// 'fa_storage_group' => encGroup,
|
|
// 'fa_storage_key' => dbw.conditional(
|
|
// [ 'img_sha1' => '' ],
|
|
// dbw.addQuotes( '' ),
|
|
// dbw.buildConcat( [ "img_sha1", encExt ] )
|
|
// ),
|
|
// 'fa_deleted_user' => encUserId,
|
|
// 'fa_deleted_timestamp' => encTimestamp,
|
|
// 'fa_deleted_reason' => encReason,
|
|
// 'fa_deleted' => this.suppress ? bitfield : 0,
|
|
// 'fa_name' => 'img_name',
|
|
// 'fa_archive_name' => 'NULL',
|
|
// 'fa_size' => 'img_size',
|
|
// 'fa_width' => 'img_width',
|
|
// 'fa_height' => 'img_height',
|
|
// 'fa_metadata' => 'img_metadata',
|
|
// 'fa_bits' => 'img_bits',
|
|
// 'fa_media_type' => 'img_media_type',
|
|
// 'fa_major_mime' => 'img_major_mime',
|
|
// 'fa_minor_mime' => 'img_minor_mime',
|
|
// 'fa_description' => 'img_description',
|
|
// 'fa_user' => 'img_user',
|
|
// 'fa_user_text' => 'img_user_text',
|
|
// 'fa_timestamp' => 'img_timestamp',
|
|
// 'fa_sha1' => 'img_sha1'
|
|
// ],
|
|
// [ 'img_name' => this.file.getName() ],
|
|
// __METHOD__
|
|
// );
|
|
// }
|
|
//
|
|
// if ( count( oldRels ) ) {
|
|
// res = dbw.select(
|
|
// 'oldimage',
|
|
// OldLocalFile::selectFields(),
|
|
// [
|
|
// 'oi_name' => this.file.getName(),
|
|
// 'oi_archive_name' => array_keys( oldRels )
|
|
// ],
|
|
// __METHOD__,
|
|
// [ 'FOR UPDATE' ]
|
|
// );
|
|
// rowsInsert = [];
|
|
// foreach ( res as row ) {
|
|
// rowsInsert[] = [
|
|
// // Deletion-specific fields
|
|
// 'fa_storage_group' => 'deleted',
|
|
// 'fa_storage_key' => ( row.oi_sha1 === '' )
|
|
// ? ''
|
|
// : "{row.oi_sha1}{dotExt}",
|
|
// 'fa_deleted_user' => this.user.getId(),
|
|
// 'fa_deleted_timestamp' => dbw.timestamp( now ),
|
|
// 'fa_deleted_reason' => this.reason,
|
|
// // Counterpart fields
|
|
// 'fa_deleted' => this.suppress ? bitfield : row.oi_deleted,
|
|
// 'fa_name' => row.oi_name,
|
|
// 'fa_archive_name' => row.oi_archive_name,
|
|
// 'fa_size' => row.oi_size,
|
|
// 'fa_width' => row.oi_width,
|
|
// 'fa_height' => row.oi_height,
|
|
// 'fa_metadata' => row.oi_metadata,
|
|
// 'fa_bits' => row.oi_bits,
|
|
// 'fa_media_type' => row.oi_media_type,
|
|
// 'fa_major_mime' => row.oi_major_mime,
|
|
// 'fa_minor_mime' => row.oi_minor_mime,
|
|
// 'fa_description' => row.oi_description,
|
|
// 'fa_user' => row.oi_user,
|
|
// 'fa_user_text' => row.oi_user_text,
|
|
// 'fa_timestamp' => row.oi_timestamp,
|
|
// 'fa_sha1' => row.oi_sha1
|
|
// ];
|
|
// }
|
|
//
|
|
// dbw.insert( 'filearchive', rowsInsert, __METHOD__ );
|
|
// }
|
|
// }
|
|
//
|
|
// function doDBDeletes() {
|
|
// dbw = this.file.repo.getMasterDB();
|
|
// list( oldRels, deleteCurrent ) = this.getOldRels();
|
|
//
|
|
// if ( count( oldRels ) ) {
|
|
// dbw.delete( 'oldimage',
|
|
// [
|
|
// 'oi_name' => this.file.getName(),
|
|
// 'oi_archive_name' => array_keys( oldRels )
|
|
// ], __METHOD__ );
|
|
// }
|
|
//
|
|
// if ( deleteCurrent ) {
|
|
// dbw.delete( 'image', [ 'img_name' => this.file.getName() ], __METHOD__ );
|
|
// }
|
|
// }
|
|
//
|
|
// /**
|
|
// * Run the transaction
|
|
// * @return Status
|
|
// */
|
|
// public function execute() {
|
|
// repo = this.file.getRepo();
|
|
// this.file.synchronized();
|
|
//
|
|
// // Prepare deletion batch
|
|
// hashes = this.getHashes();
|
|
// this.deletionBatch = [];
|
|
// ext = this.file.getExtension();
|
|
// dotExt = ext === '' ? '' : ".ext";
|
|
//
|
|
// foreach ( this.srcRels as name => srcRel ) {
|
|
// // Skip files that have no hash (e.g. missing DB record, or sha1 field and file source)
|
|
// if ( isset( hashes[name] ) ) {
|
|
// hash = hashes[name];
|
|
// key = hash . dotExt;
|
|
// dstRel = repo.getDeletedHashPath( key ) . key;
|
|
// this.deletionBatch[name] = [ srcRel, dstRel ];
|
|
// }
|
|
// }
|
|
//
|
|
// if ( !repo.hasSha1Storage() ) {
|
|
// // Removes non-existent file from the batch, so we don't get errors.
|
|
// // This also handles files in the 'deleted' zone deleted via revision deletion.
|
|
// checkStatus = this.removeNonexistentFiles( this.deletionBatch );
|
|
// if ( !checkStatus.isGood() ) {
|
|
// this.status.merge( checkStatus );
|
|
// return this.status;
|
|
// }
|
|
// this.deletionBatch = checkStatus.value;
|
|
//
|
|
// // Execute the file deletion batch
|
|
// status = this.file.repo.deleteBatch( this.deletionBatch );
|
|
// if ( !status.isGood() ) {
|
|
// this.status.merge( status );
|
|
// }
|
|
// }
|
|
//
|
|
// if ( !this.status.isOK() ) {
|
|
// // Critical file deletion error; abort
|
|
// this.file.unlock();
|
|
//
|
|
// return this.status;
|
|
// }
|
|
//
|
|
// // Copy the image/oldimage rows to filearchive
|
|
// this.doDBInserts();
|
|
// // Delete image/oldimage rows
|
|
// this.doDBDeletes();
|
|
//
|
|
// // Commit and return
|
|
// this.file.unlock();
|
|
//
|
|
// return this.status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Removes non-existent files from a deletion batch.
|
|
// * @param array batch
|
|
// * @return Status
|
|
// */
|
|
// protected function removeNonexistentFiles( batch ) {
|
|
// files = newBatch = [];
|
|
//
|
|
// foreach ( batch as batchItem ) {
|
|
// list( src, ) = batchItem;
|
|
// files[src] = this.file.repo.getVirtualUrl( 'public' ) . '/' . rawurlencode( src );
|
|
// }
|
|
//
|
|
// result = this.file.repo.fileExistsBatch( files );
|
|
// if ( in_array( null, result, true ) ) {
|
|
// return Status::newFatal( 'backend-fail-@gplx.Internal protected',
|
|
// this.file.repo.getBackend().getName() );
|
|
// }
|
|
//
|
|
// foreach ( batch as batchItem ) {
|
|
// if ( result[batchItem[0]] ) {
|
|
// newBatch[] = batchItem;
|
|
// }
|
|
// }
|
|
//
|
|
// return Status::newGood( newBatch );
|
|
// }
|
|
// }
|
|
//
|
|
// # ------------------------------------------------------------------------------
|
|
//
|
|
// /**
|
|
// * Helper class for file undeletion
|
|
// * @ingroup FileAbstraction
|
|
// */
|
|
// class LocalFileRestoreBatch {
|
|
// /** @var LocalFile */
|
|
// private file;
|
|
//
|
|
// /** @var array List of file IDs to restore */
|
|
// private cleanupBatch;
|
|
//
|
|
// /** @var array List of file IDs to restore */
|
|
// private ids;
|
|
//
|
|
// /** @var boolean Add all revisions of the file */
|
|
// private all;
|
|
//
|
|
// /** @var boolean Whether to remove all settings for suppressed fields */
|
|
// private unsuppress = false;
|
|
//
|
|
// /**
|
|
// * @param File file
|
|
// * @param boolean unsuppress
|
|
// */
|
|
// function __construct( File file, unsuppress = false ) {
|
|
// this.file = file;
|
|
// this.cleanupBatch = this.ids = [];
|
|
// this.ids = [];
|
|
// this.unsuppress = unsuppress;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Add a file by ID
|
|
// * @param int fa_id
|
|
// */
|
|
// public function addId( fa_id ) {
|
|
// this.ids[] = fa_id;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Add a whole lot of files by ID
|
|
// * @param int[] ids
|
|
// */
|
|
// public function addIds( ids ) {
|
|
// this.ids = array_merge( this.ids, ids );
|
|
// }
|
|
//
|
|
// /**
|
|
// * Add all revisions of the file
|
|
// */
|
|
// public function addAll() {
|
|
// this.all = true;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Run the transaction, except the cleanup batch.
|
|
// * The cleanup batch should be run in a separate transaction, because it locks different
|
|
// * rows and there's no need to keep the image row locked while it's acquiring those locks
|
|
// * The caller may have its own transaction open.
|
|
// * So we save the batch and let the caller call cleanup()
|
|
// * @return Status
|
|
// */
|
|
// public function execute() {
|
|
// /** @var Language */
|
|
// global wgLang;
|
|
//
|
|
// repo = this.file.getRepo();
|
|
// if ( !this.all && !this.ids ) {
|
|
// // Do nothing
|
|
// return repo.newGood();
|
|
// }
|
|
//
|
|
// lockOwnsTrx = this.file.synchronized();
|
|
//
|
|
// dbw = this.file.repo.getMasterDB();
|
|
// status = this.file.repo.newGood();
|
|
//
|
|
// exists = (boolean)dbw.selectField( 'image', '1',
|
|
// [ 'img_name' => this.file.getName() ],
|
|
// __METHOD__,
|
|
// // The synchronized() should already prevents changes, but this still may need
|
|
// // to bypass any transaction snapshot. However, if synchronized() started the
|
|
// // trx (which it probably did) then snapshot is post-synchronized and up-to-date.
|
|
// lockOwnsTrx ? [] : [ 'LOCK IN SHARE MODE' ]
|
|
// );
|
|
//
|
|
// // Fetch all or selected archived revisions for the file,
|
|
// // sorted from the most recent to the oldest.
|
|
// conditions = [ 'fa_name' => this.file.getName() ];
|
|
//
|
|
// if ( !this.all ) {
|
|
// conditions['fa_id'] = this.ids;
|
|
// }
|
|
//
|
|
// result = dbw.select(
|
|
// 'filearchive',
|
|
// ArchivedFile::selectFields(),
|
|
// conditions,
|
|
// __METHOD__,
|
|
// [ 'ORDER BY' => 'fa_timestamp DESC' ]
|
|
// );
|
|
//
|
|
// idsPresent = [];
|
|
// storeBatch = [];
|
|
// insertBatch = [];
|
|
// insertCurrent = false;
|
|
// deleteIds = [];
|
|
// first = true;
|
|
// archiveNames = [];
|
|
//
|
|
// foreach ( result as row ) {
|
|
// idsPresent[] = row.fa_id;
|
|
//
|
|
// if ( row.fa_name != this.file.getName() ) {
|
|
// status.error( 'undelete-filename-mismatch', wgLang.timeanddate( row.fa_timestamp ) );
|
|
// status.failCount++;
|
|
// continue;
|
|
// }
|
|
//
|
|
// if ( row.fa_storage_key == '' ) {
|
|
// // Revision was missing pre-deletion
|
|
// status.error( 'undelete-bad-store-key', wgLang.timeanddate( row.fa_timestamp ) );
|
|
// status.failCount++;
|
|
// continue;
|
|
// }
|
|
//
|
|
// deletedRel = repo.getDeletedHashPath( row.fa_storage_key ) .
|
|
// row.fa_storage_key;
|
|
// deletedUrl = repo.getVirtualUrl() . '/deleted/' . deletedRel;
|
|
//
|
|
// if ( isset( row.fa_sha1 ) ) {
|
|
// sha1 = row.fa_sha1;
|
|
// } else {
|
|
// // old row, populate from key
|
|
// sha1 = LocalRepo::getHashFromKey( row.fa_storage_key );
|
|
// }
|
|
//
|
|
// # Fix leading zero
|
|
// if ( strlen( sha1 ) == 32 && sha1[0] == '0' ) {
|
|
// sha1 = substr( sha1, 1 );
|
|
// }
|
|
//
|
|
// if ( is_null( row.fa_major_mime ) || row.fa_major_mime == 'unknown'
|
|
// || is_null( row.fa_minor_mime ) || row.fa_minor_mime == 'unknown'
|
|
// || is_null( row.fa_media_type ) || row.fa_media_type == 'UNKNOWN'
|
|
// || is_null( row.fa_metadata )
|
|
// ) {
|
|
// // Refresh our metadata
|
|
// // Required for a new current revision; nice for older ones too. :)
|
|
// props = RepoGroup::singleton().getFileProps( deletedUrl );
|
|
// } else {
|
|
// props = [
|
|
// 'minor_mime' => row.fa_minor_mime,
|
|
// 'major_mime' => row.fa_major_mime,
|
|
// 'media_type' => row.fa_media_type,
|
|
// 'metadata' => row.fa_metadata
|
|
// ];
|
|
// }
|
|
//
|
|
// if ( first && !exists ) {
|
|
// // This revision will be published as the new current version
|
|
// destRel = this.file.getRel();
|
|
// insertCurrent = [
|
|
// 'img_name' => row.fa_name,
|
|
// 'img_size' => row.fa_size,
|
|
// 'img_width' => row.fa_width,
|
|
// 'img_height' => row.fa_height,
|
|
// 'img_metadata' => props['metadata'],
|
|
// 'img_bits' => row.fa_bits,
|
|
// 'img_media_type' => props['media_type'],
|
|
// 'img_major_mime' => props['major_mime'],
|
|
// 'img_minor_mime' => props['minor_mime'],
|
|
// 'img_description' => row.fa_description,
|
|
// 'img_user' => row.fa_user,
|
|
// 'img_user_text' => row.fa_user_text,
|
|
// 'img_timestamp' => row.fa_timestamp,
|
|
// 'img_sha1' => sha1
|
|
// ];
|
|
//
|
|
// // The live (current) version cannot be hidden!
|
|
// if ( !this.unsuppress && row.fa_deleted ) {
|
|
// status.fatal( 'undeleterevdel' );
|
|
// this.file.unlock();
|
|
// return status;
|
|
// }
|
|
// } else {
|
|
// archiveName = row.fa_archive_name;
|
|
//
|
|
// if ( archiveName == '' ) {
|
|
// // This was originally a current version; we
|
|
// // have to devise a new archive name for it.
|
|
// // Format is <timestamp of archiving>!<name>
|
|
// timestamp = wfTimestamp( TS_UNIX, row.fa_deleted_timestamp );
|
|
//
|
|
// do {
|
|
// archiveName = wfTimestamp( TS_MW, timestamp ) . '!' . row.fa_name;
|
|
// timestamp++;
|
|
// } while ( isset( archiveNames[archiveName] ) );
|
|
// }
|
|
//
|
|
// archiveNames[archiveName] = true;
|
|
// destRel = this.file.getArchiveRel( archiveName );
|
|
// insertBatch[] = [
|
|
// 'oi_name' => row.fa_name,
|
|
// 'oi_archive_name' => archiveName,
|
|
// 'oi_size' => row.fa_size,
|
|
// 'oi_width' => row.fa_width,
|
|
// 'oi_height' => row.fa_height,
|
|
// 'oi_bits' => row.fa_bits,
|
|
// 'oi_description' => row.fa_description,
|
|
// 'oi_user' => row.fa_user,
|
|
// 'oi_user_text' => row.fa_user_text,
|
|
// 'oi_timestamp' => row.fa_timestamp,
|
|
// 'oi_metadata' => props['metadata'],
|
|
// 'oi_media_type' => props['media_type'],
|
|
// 'oi_major_mime' => props['major_mime'],
|
|
// 'oi_minor_mime' => props['minor_mime'],
|
|
// 'oi_deleted' => this.unsuppress ? 0 : row.fa_deleted,
|
|
// 'oi_sha1' => sha1 ];
|
|
// }
|
|
//
|
|
// deleteIds[] = row.fa_id;
|
|
//
|
|
// if ( !this.unsuppress && row.fa_deleted & File::DELETED_FILE ) {
|
|
// // private files can stay where they are
|
|
// status.successCount++;
|
|
// } else {
|
|
// storeBatch[] = [ deletedUrl, 'public', destRel ];
|
|
// this.cleanupBatch[] = row.fa_storage_key;
|
|
// }
|
|
//
|
|
// first = false;
|
|
// }
|
|
//
|
|
// unset( result );
|
|
//
|
|
// // Add a warning to the status Object for missing IDs
|
|
// missingIds = array_diff( this.ids, idsPresent );
|
|
//
|
|
// foreach ( missingIds as id ) {
|
|
// status.error( 'undelete-missing-filearchive', id );
|
|
// }
|
|
//
|
|
// if ( !repo.hasSha1Storage() ) {
|
|
// // Remove missing files from batch, so we don't get errors when undeleting them
|
|
// checkStatus = this.removeNonexistentFiles( storeBatch );
|
|
// if ( !checkStatus.isGood() ) {
|
|
// status.merge( checkStatus );
|
|
// return status;
|
|
// }
|
|
// storeBatch = checkStatus.value;
|
|
//
|
|
// // Run the store batch
|
|
// // Use the OVERWRITE_SAME flag to smooth over a common error
|
|
// storeStatus = this.file.repo.storeBatch( storeBatch, FileRepo::OVERWRITE_SAME );
|
|
// status.merge( storeStatus );
|
|
//
|
|
// if ( !status.isGood() ) {
|
|
// // Even if some files could be copied, fail entirely as that is the
|
|
// // easiest thing to do without data loss
|
|
// this.cleanupFailedBatch( storeStatus, storeBatch );
|
|
// status.setOK( false );
|
|
// this.file.unlock();
|
|
//
|
|
// return status;
|
|
// }
|
|
// }
|
|
//
|
|
// // Run the DB updates
|
|
// // Because we have locked the image row, key conflicts should be rare.
|
|
// // If they do occur, we can roll back the transaction at this time with
|
|
// // no data loss, but leaving unregistered files scattered throughout the
|
|
// // public zone.
|
|
// // This is not ideal, which is why it's important to synchronized the image row.
|
|
// if ( insertCurrent ) {
|
|
// dbw.insert( 'image', insertCurrent, __METHOD__ );
|
|
// }
|
|
//
|
|
// if ( insertBatch ) {
|
|
// dbw.insert( 'oldimage', insertBatch, __METHOD__ );
|
|
// }
|
|
//
|
|
// if ( deleteIds ) {
|
|
// dbw.delete( 'filearchive',
|
|
// [ 'fa_id' => deleteIds ],
|
|
// __METHOD__ );
|
|
// }
|
|
//
|
|
// // If store batch is empty (all files are missing), deletion is to be considered successful
|
|
// if ( status.successCount > 0 || !storeBatch || repo.hasSha1Storage() ) {
|
|
// if ( !exists ) {
|
|
// wfDebug( __METHOD__ . " restored {status.successCount} items, creating a new current\n" );
|
|
//
|
|
// DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
|
|
//
|
|
// this.file.purgeEverything();
|
|
// } else {
|
|
// wfDebug( __METHOD__ . " restored {status.successCount} as archived versions\n" );
|
|
// this.file.purgeDescription();
|
|
// }
|
|
// }
|
|
//
|
|
// this.file.unlock();
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Removes non-existent files from a store batch.
|
|
// * @param array triplets
|
|
// * @return Status
|
|
// */
|
|
// protected function removeNonexistentFiles( triplets ) {
|
|
// files = filteredTriplets = [];
|
|
// foreach ( triplets as file ) {
|
|
// files[file[0]] = file[0];
|
|
// }
|
|
//
|
|
// result = this.file.repo.fileExistsBatch( files );
|
|
// if ( in_array( null, result, true ) ) {
|
|
// return Status::newFatal( 'backend-fail-@gplx.Internal protected',
|
|
// this.file.repo.getBackend().getName() );
|
|
// }
|
|
//
|
|
// foreach ( triplets as file ) {
|
|
// if ( result[file[0]] ) {
|
|
// filteredTriplets[] = file;
|
|
// }
|
|
// }
|
|
//
|
|
// return Status::newGood( filteredTriplets );
|
|
// }
|
|
//
|
|
// /**
|
|
// * Removes non-existent files from a cleanup batch.
|
|
// * @param array batch
|
|
// * @return array
|
|
// */
|
|
// protected function removeNonexistentFromCleanup( batch ) {
|
|
// files = newBatch = [];
|
|
// repo = this.file.repo;
|
|
//
|
|
// foreach ( batch as file ) {
|
|
// files[file] = repo.getVirtualUrl( 'deleted' ) . '/' .
|
|
// rawurlencode( repo.getDeletedHashPath( file ) . file );
|
|
// }
|
|
//
|
|
// result = repo.fileExistsBatch( files );
|
|
//
|
|
// foreach ( batch as file ) {
|
|
// if ( result[file] ) {
|
|
// newBatch[] = file;
|
|
// }
|
|
// }
|
|
//
|
|
// return newBatch;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Delete unused files in the deleted zone.
|
|
// * This should be called from outside the transaction in which execute() was called.
|
|
// * @return Status
|
|
// */
|
|
// public function cleanup() {
|
|
// if ( !this.cleanupBatch ) {
|
|
// return this.file.repo.newGood();
|
|
// }
|
|
//
|
|
// this.cleanupBatch = this.removeNonexistentFromCleanup( this.cleanupBatch );
|
|
//
|
|
// status = this.file.repo.cleanupDeletedBatch( this.cleanupBatch );
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Cleanup a failed batch. The batch was only partially successful, so
|
|
// * rollback by removing all items that were succesfully copied.
|
|
// *
|
|
// * @param Status storeStatus
|
|
// * @param array storeBatch
|
|
// */
|
|
// protected function cleanupFailedBatch( storeStatus, storeBatch ) {
|
|
// cleanupBatch = [];
|
|
//
|
|
// foreach ( storeStatus.success as i => success ) {
|
|
// // Check if this item of the batch was successfully copied
|
|
// if ( success ) {
|
|
// // Item was successfully copied and needs to be removed again
|
|
// // Extract (dstZone, dstRel) from the batch
|
|
// cleanupBatch[] = [ storeBatch[i][1], storeBatch[i][2] ];
|
|
// }
|
|
// }
|
|
// this.file.repo.cleanupBatch( cleanupBatch );
|
|
// }
|
|
// }
|
|
//
|
|
// # ------------------------------------------------------------------------------
|
|
//
|
|
// /**
|
|
// * Helper class for file movement
|
|
// * @ingroup FileAbstraction
|
|
// */
|
|
// class LocalFileMoveBatch {
|
|
// /** @var LocalFile */
|
|
// protected file;
|
|
//
|
|
// /** @var Title */
|
|
// protected target;
|
|
//
|
|
// protected cur;
|
|
//
|
|
// protected olds;
|
|
//
|
|
// protected oldCount;
|
|
//
|
|
// protected archive;
|
|
//
|
|
// /** @var IDatabase */
|
|
// protected db;
|
|
//
|
|
// /**
|
|
// * @param File file
|
|
// * @param Title target
|
|
// */
|
|
// function __construct( File file, Title target ) {
|
|
// this.file = file;
|
|
// this.target = target;
|
|
// this.oldHash = this.file.repo.getHashPath( this.file.getName() );
|
|
// this.newHash = this.file.repo.getHashPath( this.target.getDBkey() );
|
|
// this.oldName = this.file.getName();
|
|
// this.newName = this.file.repo.getNameFromTitle( this.target );
|
|
// this.oldRel = this.oldHash . this.oldName;
|
|
// this.newRel = this.newHash . this.newName;
|
|
// this.db = file.getRepo().getMasterDB();
|
|
// }
|
|
//
|
|
// /**
|
|
// * Add the current image to the batch
|
|
// */
|
|
// public function addCurrent() {
|
|
// this.cur = [ this.oldRel, this.newRel ];
|
|
// }
|
|
//
|
|
// /**
|
|
// * Add the old versions of the image to the batch
|
|
// * @return array List of archive names from old versions
|
|
// */
|
|
// public function addOlds() {
|
|
// archiveBase = 'archive';
|
|
// this.olds = [];
|
|
// this.oldCount = 0;
|
|
// archiveNames = [];
|
|
//
|
|
// result = this.db.select( 'oldimage',
|
|
// [ 'oi_archive_name', 'oi_deleted' ],
|
|
// [ 'oi_name' => this.oldName ],
|
|
// __METHOD__,
|
|
// [ 'LOCK IN SHARE MODE' ] // ignore snapshot
|
|
// );
|
|
//
|
|
// foreach ( result as row ) {
|
|
// archiveNames[] = row.oi_archive_name;
|
|
// oldName = row.oi_archive_name;
|
|
// bits = explode( '!', oldName, 2 );
|
|
//
|
|
// if ( count( bits ) != 2 ) {
|
|
// wfDebug( "Old file name missing !: 'oldName' \n" );
|
|
// continue;
|
|
// }
|
|
//
|
|
// list( timestamp, filename ) = bits;
|
|
//
|
|
// if ( this.oldName != filename ) {
|
|
// wfDebug( "Old file name doesn't match: 'oldName' \n" );
|
|
// continue;
|
|
// }
|
|
//
|
|
// this.oldCount++;
|
|
//
|
|
// // Do we want to add those to oldCount?
|
|
// if ( row.oi_deleted & File::DELETED_FILE ) {
|
|
// continue;
|
|
// }
|
|
//
|
|
// this.olds[] = [
|
|
// "{archiveBase}/{this.oldHash}{oldName}",
|
|
// "{archiveBase}/{this.newHash}{timestamp}!{this.newName}"
|
|
// ];
|
|
// }
|
|
//
|
|
// return archiveNames;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Perform the move.
|
|
// * @return Status
|
|
// */
|
|
// public function execute() {
|
|
// repo = this.file.repo;
|
|
// status = repo.newGood();
|
|
// destFile = wfLocalFile( this.target );
|
|
//
|
|
// this.file.synchronized(); // begin
|
|
// destFile.synchronized(); // quickly fail if destination is not available
|
|
//
|
|
// triplets = this.getMoveTriplets();
|
|
// checkStatus = this.removeNonexistentFiles( triplets );
|
|
// if ( !checkStatus.isGood() ) {
|
|
// destFile.unlock();
|
|
// this.file.unlock();
|
|
// status.merge( checkStatus ); // couldn't talk to file backend
|
|
// return status;
|
|
// }
|
|
// triplets = checkStatus.value;
|
|
//
|
|
// // Verify the file versions metadata in the DB.
|
|
// statusDb = this.verifyDBUpdates();
|
|
// if ( !statusDb.isGood() ) {
|
|
// destFile.unlock();
|
|
// this.file.unlock();
|
|
// statusDb.setOK( false );
|
|
//
|
|
// return statusDb;
|
|
// }
|
|
//
|
|
// if ( !repo.hasSha1Storage() ) {
|
|
// // Copy the files into their new location.
|
|
// // If a prior process fataled copying or cleaning up files we tolerate any
|
|
// // of the existing files if they are identical to the ones being stored.
|
|
// statusMove = repo.storeBatch( triplets, FileRepo::OVERWRITE_SAME );
|
|
// wfDebugLog( 'imagemove', "Moved files for {this.file.getName()}: " .
|
|
// "{statusMove.successCount} successes, {statusMove.failCount} failures" );
|
|
// if ( !statusMove.isGood() ) {
|
|
// // Delete any files copied over (while the destination is still locked)
|
|
// this.cleanupTarget( triplets );
|
|
// destFile.unlock();
|
|
// this.file.unlock();
|
|
// wfDebugLog( 'imagemove', "Error in moving files: "
|
|
// . statusMove.getWikiText( false, false, 'en' ) );
|
|
// statusMove.setOK( false );
|
|
//
|
|
// return statusMove;
|
|
// }
|
|
// status.merge( statusMove );
|
|
// }
|
|
//
|
|
// // Rename the file versions metadata in the DB.
|
|
// this.doDBUpdates();
|
|
//
|
|
// wfDebugLog( 'imagemove', "Renamed {this.file.getName()} in database: " .
|
|
// "{statusDb.successCount} successes, {statusDb.failCount} failures" );
|
|
//
|
|
// destFile.unlock();
|
|
// this.file.unlock(); // done
|
|
//
|
|
// // Everything went ok, remove the source files
|
|
// this.cleanupSource( triplets );
|
|
//
|
|
// status.merge( statusDb );
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Verify the database updates and return a new Status indicating how
|
|
// * many rows would be updated.
|
|
// *
|
|
// * @return Status
|
|
// */
|
|
// protected function verifyDBUpdates() {
|
|
// repo = this.file.repo;
|
|
// status = repo.newGood();
|
|
// dbw = this.db;
|
|
//
|
|
// hasCurrent = dbw.selectField(
|
|
// 'image',
|
|
// '1',
|
|
// [ 'img_name' => this.oldName ],
|
|
// __METHOD__,
|
|
// [ 'FOR UPDATE' ]
|
|
// );
|
|
// oldRowCount = dbw.selectField(
|
|
// 'oldimage',
|
|
// 'COUNT(*)',
|
|
// [ 'oi_name' => this.oldName ],
|
|
// __METHOD__,
|
|
// [ 'FOR UPDATE' ]
|
|
// );
|
|
//
|
|
// if ( hasCurrent ) {
|
|
// status.successCount++;
|
|
// } else {
|
|
// status.failCount++;
|
|
// }
|
|
// status.successCount += oldRowCount;
|
|
// // Bug 34934: oldCount is based on files that actually exist.
|
|
// // There may be more DB rows than such files, in which case affected
|
|
// // can be greater than total. We use max() to avoid negatives here.
|
|
// status.failCount += max( 0, this.oldCount - oldRowCount );
|
|
// if ( status.failCount ) {
|
|
// status.error( 'imageinvalidfilename' );
|
|
// }
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Do the database updates and return a new Status indicating how
|
|
// * many rows where updated.
|
|
// */
|
|
// protected function doDBUpdates() {
|
|
// dbw = this.db;
|
|
//
|
|
// // Update current image
|
|
// dbw.update(
|
|
// 'image',
|
|
// [ 'img_name' => this.newName ],
|
|
// [ 'img_name' => this.oldName ],
|
|
// __METHOD__
|
|
// );
|
|
// // Update old images
|
|
// dbw.update(
|
|
// 'oldimage',
|
|
// [
|
|
// 'oi_name' => this.newName,
|
|
// 'oi_archive_name = ' . dbw.strreplace( 'oi_archive_name',
|
|
// dbw.addQuotes( this.oldName ), dbw.addQuotes( this.newName ) ),
|
|
// ],
|
|
// [ 'oi_name' => this.oldName ],
|
|
// __METHOD__
|
|
// );
|
|
// }
|
|
//
|
|
// /**
|
|
// * Generate triplets for FileRepo::storeBatch().
|
|
// * @return array
|
|
// */
|
|
// protected function getMoveTriplets() {
|
|
// moves = array_merge( [ this.cur ], this.olds );
|
|
// triplets = []; // The format is: (srcUrl, destZone, destUrl)
|
|
//
|
|
// foreach ( moves as move ) {
|
|
// // move: (oldRelativePath, newRelativePath)
|
|
// srcUrl = this.file.repo.getVirtualUrl() . '/public/' . rawurlencode( move[0] );
|
|
// triplets[] = [ srcUrl, 'public', move[1] ];
|
|
// wfDebugLog(
|
|
// 'imagemove',
|
|
// "Generated move triplet for {this.file.getName()}: {srcUrl} :: public :: {move[1]}"
|
|
// );
|
|
// }
|
|
//
|
|
// return triplets;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Removes non-existent files from move batch.
|
|
// * @param array triplets
|
|
// * @return Status
|
|
// */
|
|
// protected function removeNonexistentFiles( triplets ) {
|
|
// files = [];
|
|
//
|
|
// foreach ( triplets as file ) {
|
|
// files[file[0]] = file[0];
|
|
// }
|
|
//
|
|
// result = this.file.repo.fileExistsBatch( files );
|
|
// if ( in_array( null, result, true ) ) {
|
|
// return Status::newFatal( 'backend-fail-@gplx.Internal protected',
|
|
// this.file.repo.getBackend().getName() );
|
|
// }
|
|
//
|
|
// filteredTriplets = [];
|
|
// foreach ( triplets as file ) {
|
|
// if ( result[file[0]] ) {
|
|
// filteredTriplets[] = file;
|
|
// } else {
|
|
// wfDebugLog( 'imagemove', "File {file[0]} does not exist" );
|
|
// }
|
|
// }
|
|
//
|
|
// return Status::newGood( filteredTriplets );
|
|
// }
|
|
//
|
|
// /**
|
|
// * Cleanup a partially moved array of triplets by deleting the target
|
|
// * files. Called if something went wrong half way.
|
|
// * @param array triplets
|
|
// */
|
|
// protected function cleanupTarget( triplets ) {
|
|
// // Create dest pairs from the triplets
|
|
// pairs = [];
|
|
// foreach ( triplets as triplet ) {
|
|
// // triplet: (old source virtual URL, dst zone, dest rel)
|
|
// pairs[] = [ triplet[1], triplet[2] ];
|
|
// }
|
|
//
|
|
// this.file.repo.cleanupBatch( pairs );
|
|
// }
|
|
//
|
|
// /**
|
|
// * Cleanup a fully moved array of triplets by deleting the source files.
|
|
// * Called at the end of the move process if everything else went ok.
|
|
// * @param array triplets
|
|
// */
|
|
// protected function cleanupSource( triplets ) {
|
|
// // Create source file names from the triplets
|
|
// files = [];
|
|
// foreach ( triplets as triplet ) {
|
|
// files[] = triplet[0];
|
|
// }
|
|
//
|
|
// this.file.repo.cleanupBatch( files );
|
|
// }
|
|
}
|