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.
1923 lines
60 KiB
1923 lines
60 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; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.includes.*;
|
|
import gplx.xowa.mediawiki.includes.filerepo.file.*;
|
|
/* TODO.XO:
|
|
* getZoneUrl
|
|
*/
|
|
public class XomwFileRepo {
|
|
// static final DELETE_SOURCE = 1;
|
|
// static final OVERWRITE = 2;
|
|
// static final OVERWRITE_SAME = 4;
|
|
// static final SKIP_LOCKING = 8;
|
|
//
|
|
// static final NAME_AND_TIME_ONLY = 1;
|
|
//
|
|
// /** @var boolean Whether to fetch commons image description pages and display
|
|
// * them on the local wiki */
|
|
// public fetchDescription;
|
|
//
|
|
// /** @var int */
|
|
// public descriptionCacheExpiry;
|
|
//
|
|
// /** @var boolean */
|
|
// protected hasSha1Storage = false;
|
|
//
|
|
// /** @var boolean */
|
|
// protected supportsSha1URLs = false;
|
|
//
|
|
// /** @var FileBackend */
|
|
// protected backend;
|
|
//
|
|
// /** @var array Map of zones to config */
|
|
// protected zones = [];
|
|
//
|
|
// /** @var String URL of thumb.php */
|
|
// protected thumbScriptUrl;
|
|
//
|
|
// /** @var boolean Whether to skip media file transformation on parse and rely
|
|
// * on a 404 handler instead. */
|
|
// protected transformVia404;
|
|
//
|
|
// /** @var String URL of image description pages, e.g.
|
|
// * https://en.wikipedia.org/wiki/File:
|
|
// */
|
|
// protected descBaseUrl;
|
|
//
|
|
// /** @var String URL of the MediaWiki installation, equivalent to
|
|
// * wgScriptPath, e.g. https://en.wikipedia.org/w
|
|
// */
|
|
// protected scriptDirUrl;
|
|
//
|
|
// /** @var String Script extension of the MediaWiki installation, equivalent
|
|
// * to the old wgScriptExtension, e.g. .php5 defaults to .php */
|
|
// protected scriptExtension;
|
|
//
|
|
// /** @var String Equivalent to wgArticlePath, e.g. https://en.wikipedia.org/wiki/1 */
|
|
// protected articleUrl;
|
|
//
|
|
// /** @var boolean Equivalent to wgCapitalLinks (or wgCapitalLinkOverrides[NS_FILE],
|
|
// * determines whether filenames implicitly start with a capital letter.
|
|
// * The current implementation may give incorrect description page links
|
|
// * when the local wgCapitalLinks and initialCapital are mismatched.
|
|
// */
|
|
// protected initialCapital;
|
|
//
|
|
// /** @var String May be 'paranoid' to remove all parameters from error
|
|
// * messages, 'none' to leave the paths in unchanged, or 'simple' to
|
|
// * replace paths with placeholders. Default for LocalRepo is
|
|
// * 'simple'.
|
|
// */
|
|
// protected pathDisclosureProtection = 'simple';
|
|
|
|
/** @var String|false Public zone URL. */
|
|
public byte[] url;
|
|
|
|
/** @var String The super thumbnail URL. Defaults to "<url>/thumb". */
|
|
public byte[] thumbUrl;
|
|
|
|
/** @var int The number of directory levels for hash-based division of files */
|
|
private int hashLevels = 2;
|
|
|
|
// /** @var int The number of directory levels for hash-based division of deleted files */
|
|
// protected deletedHashLevels;
|
|
//
|
|
// /** @var int File names over this size will use the short form of thumbnail
|
|
// * names. Short thumbnail names only have the width, parameters, and the
|
|
// * extension.
|
|
// */
|
|
// protected abbrvThreshold;
|
|
//
|
|
// /** @var String The URL of the repo's favicon, if any */
|
|
// protected favicon;
|
|
//
|
|
// /** @var boolean Whether all zones should be private (e.g. private wiki repo) */
|
|
// protected isPrivate;
|
|
//
|
|
// /** @var array callable Override these in the super class */
|
|
// protected fileFactory = [ 'UnregisteredLocalFile', 'newFromTitle' ];
|
|
// /** @var array callable|boolean Override these in the super class */
|
|
// protected oldFileFactory = false;
|
|
// /** @var array callable|boolean Override these in the super class */
|
|
// protected fileFactoryKey = false;
|
|
// /** @var array callable|boolean Override these in the super class */
|
|
// protected oldFileFactoryKey = false;
|
|
|
|
public XomwFileRepo(byte[] url, byte[] thumbUrl) {
|
|
this.url = url;
|
|
this.thumbUrl = thumbUrl;
|
|
}
|
|
// /**
|
|
// * @param array|null info
|
|
// * @throws MWException
|
|
// */
|
|
// public function __construct(array info = null) {
|
|
// // Verify required settings presence
|
|
// if (
|
|
// info === null
|
|
// || !array_key_exists('name', info)
|
|
// || !array_key_exists('backend', info)
|
|
// ) {
|
|
// throw new MWException(__CLASS__ .
|
|
// " requires an array of options having both 'name' and 'backend' keys.\n");
|
|
// }
|
|
//
|
|
// // Required settings
|
|
// this.name = info['name'];
|
|
// if (info['backend'] instanceof FileBackend) {
|
|
// this.backend = info['backend']; // useful for testing
|
|
// } else {
|
|
// this.backend = FileBackendGroup::singleton().get(info['backend']);
|
|
// }
|
|
//
|
|
// // Optional settings that can have no value
|
|
// optionalSettings = [
|
|
// 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
|
|
// 'thumbScriptUrl', 'pathDisclosureProtection', 'descriptionCacheExpiry',
|
|
// 'scriptExtension', 'favicon'
|
|
// ];
|
|
// foreach (optionalSettings as var) {
|
|
// if (isset(info[var])) {
|
|
// this.var = info[var];
|
|
// }
|
|
// }
|
|
//
|
|
// // Optional settings that have a default
|
|
// this.initialCapital = isset(info['initialCapital'])
|
|
// ? info['initialCapital']
|
|
// : XomwNamespace::isCapitalized(NS_FILE);
|
|
// this.url = isset(info['url'])
|
|
// ? info['url']
|
|
// : false; // a subclass may set the URL (e.g. ForeignAPIRepo)
|
|
// if (isset(info['thumbUrl'])) {
|
|
// this.thumbUrl = info['thumbUrl'];
|
|
// } else {
|
|
// this.thumbUrl = this.url ? "{this.url}/thumb" : false;
|
|
// }
|
|
// this.hashLevels = isset(info['hashLevels'])
|
|
// ? info['hashLevels']
|
|
// : 2;
|
|
// this.deletedHashLevels = isset(info['deletedHashLevels'])
|
|
// ? info['deletedHashLevels']
|
|
// : this.hashLevels;
|
|
// this.transformVia404 = !empty(info['transformVia404']);
|
|
// this.abbrvThreshold = isset(info['abbrvThreshold'])
|
|
// ? info['abbrvThreshold']
|
|
// : 255;
|
|
// this.isPrivate = !empty(info['isPrivate']);
|
|
// // Give defaults for the basic zones...
|
|
// this.zones = isset(info['zones']) ? info['zones'] : [];
|
|
// foreach ([ 'public', 'thumb', 'transcoded', 'temp', 'deleted' ] as zone) {
|
|
// if (!isset(this.zones[zone]['container'])) {
|
|
// this.zones[zone]['container'] = "{this.name}-{zone}";
|
|
// }
|
|
// if (!isset(this.zones[zone]['directory'])) {
|
|
// this.zones[zone]['directory'] = '';
|
|
// }
|
|
// if (!isset(this.zones[zone]['urlsByExt'])) {
|
|
// this.zones[zone]['urlsByExt'] = [];
|
|
// }
|
|
// }
|
|
//
|
|
// this.supportsSha1URLs = !empty(info['supportsSha1URLs']);
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get the file backend instance. Use this function wisely.
|
|
// *
|
|
// * @return FileBackend
|
|
// */
|
|
// public function getBackend() {
|
|
// return this.backend;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get an explanatory message if this repo is read-only.
|
|
// * This checks if an administrator disabled writes to the backend.
|
|
// *
|
|
// * @return String|boolean Returns false if the repo is not read-only
|
|
// */
|
|
// public function getReadOnlyReason() {
|
|
// return this.backend.getReadOnlyReason();
|
|
// }
|
|
//
|
|
// /**
|
|
// * Check if a single zone or list of zones is defined for usage
|
|
// *
|
|
// * @param array doZones Only do a particular zones
|
|
// * @throws MWException
|
|
// * @return Status
|
|
// */
|
|
// protected function initZones(doZones = []) {
|
|
// status = this.newGood();
|
|
// foreach ((array)doZones as zone) {
|
|
// root = this.getZonePath(zone);
|
|
// if (root === null) {
|
|
// throw new MWException("No 'zone' zone defined in the {this.name} repo.");
|
|
// }
|
|
// }
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Determine if a String is an mwrepo:// URL
|
|
// *
|
|
// * @param String url
|
|
// * @return boolean
|
|
// */
|
|
// public static function isVirtualUrl(url) {
|
|
// return substr(url, 0, 9) == 'mwrepo://';
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get a URL referring to this repository, with the private mwrepo protocol.
|
|
// * The suffix, if supplied, is considered to be unencoded, and will be
|
|
// * URL-encoded before being returned.
|
|
// *
|
|
// * @param String|boolean suffix
|
|
// * @return String
|
|
// */
|
|
// public function getVirtualUrl(suffix = false) {
|
|
// path = 'mwrepo://' . this.name;
|
|
// if (suffix !== false) {
|
|
// path .= '/' . rawurlencode(suffix);
|
|
// }
|
|
//
|
|
// return path;
|
|
// }
|
|
|
|
/**
|
|
* Get the URL corresponding to one of the four basic zones
|
|
*
|
|
* @param String zone One of: public, deleted, temp, thumb
|
|
* @param String|null ext Optional file extension
|
|
* @return String|boolean
|
|
*/
|
|
public byte[] getZoneUrl(int zone, byte[] ext) {
|
|
// XO.MW.UNSUPPORTED.CUSTOM:ignore customized zones by ext
|
|
//if (in_array(zone, [ 'public', 'thumb', 'transcoded' ])) {
|
|
// // standard public zones
|
|
// if (ext !== null && isset(this.zones[zone]['urlsByExt'][ext])) {
|
|
// // custom URL for extension/zone
|
|
// return this.zones[zone]['urlsByExt'][ext];
|
|
// } elseif (isset(this.zones[zone]['url'])) {
|
|
// // custom URL for zone
|
|
// return this.zones[zone]['url'];
|
|
// }
|
|
//}
|
|
switch (zone) {
|
|
case Zone__public:
|
|
return this.url;
|
|
//case 'temp':
|
|
//case 'deleted':
|
|
// return false; // no public URL
|
|
case Zone__thumb:
|
|
return this.thumbUrl;
|
|
//case 'transcoded':
|
|
// return "{this.url}/transcoded";
|
|
//default:
|
|
// return false;
|
|
}
|
|
return url;
|
|
}
|
|
|
|
// /**
|
|
// * @return boolean Whether non-ASCII path characters are allowed
|
|
// */
|
|
// public function backendSupportsUnicodePaths() {
|
|
// return (boolean)(this.getBackend().getFeatures() & FileBackend::ATTR_UNICODE_PATHS);
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get the backend storage path corresponding to a virtual URL.
|
|
// * Use this function wisely.
|
|
// *
|
|
// * @param String url
|
|
// * @throws MWException
|
|
// * @return String
|
|
// */
|
|
// public function resolveVirtualUrl(url) {
|
|
// if (substr(url, 0, 9) != 'mwrepo://') {
|
|
// throw new MWException(__METHOD__ . ': unknown protocol');
|
|
// }
|
|
// bits = explode('/', substr(url, 9), 3);
|
|
// if (count(bits) != 3) {
|
|
// throw new MWException(__METHOD__ . ": invalid mwrepo URL: url");
|
|
// }
|
|
// list(repo, zone, rel) = bits;
|
|
// if (repo !== this.name) {
|
|
// throw new MWException(__METHOD__ . ": fetching from a foreign repo is not supported");
|
|
// }
|
|
// super = this.getZonePath(zone);
|
|
// if (!super) {
|
|
// throw new MWException(__METHOD__ . ": invalid zone: zone");
|
|
// }
|
|
//
|
|
// return super . '/' . rawurldecode(rel);
|
|
// }
|
|
//
|
|
// /**
|
|
// * The the storage container and super path of a zone
|
|
// *
|
|
// * @param String zone
|
|
// * @return array (container, super path) or (null, null)
|
|
// */
|
|
// protected function getZoneLocation(zone) {
|
|
// if (!isset(this.zones[zone])) {
|
|
// return [ null, null ]; // bogus
|
|
// }
|
|
//
|
|
// return [ this.zones[zone]['container'], this.zones[zone]['directory'] ];
|
|
// }
|
|
|
|
/**
|
|
* Get the storage path corresponding to one of the zones
|
|
*
|
|
* @param String zone
|
|
* @return String|null Returns null if the zone is not defined
|
|
*/
|
|
public byte[] getZonePath(int zone) {
|
|
// list(container, super) = this.getZoneLocation(zone);
|
|
// if (container === null || super === null) {
|
|
// return null;
|
|
// }
|
|
// backendName = this.backend.getName();
|
|
// if (super != '') { // may not be set
|
|
// super = "/{super}";
|
|
// }
|
|
//
|
|
// return "mwstore://backendName/{container}{super}";
|
|
return Bry_.Empty;
|
|
}
|
|
|
|
// /**
|
|
// * Create a new File Object from the local repository
|
|
// *
|
|
// * @param Title|String title Title Object or String
|
|
// * @param boolean|String time Time at which the image was uploaded. If this
|
|
// * is specified, the returned Object will be an instance of the
|
|
// * repository's old file class instead of a current file. Repositories
|
|
// * not supporting version control should return false if this parameter
|
|
// * is set.
|
|
// * @return File|null A File, or null if passed an invalid Title
|
|
// */
|
|
// public function newFile(title, time = false) {
|
|
// title = File::normalizeTitle(title);
|
|
// if (!title) {
|
|
// return null;
|
|
// }
|
|
// if (time) {
|
|
// if (this.oldFileFactory) {
|
|
// return call_user_func(this.oldFileFactory, title, this, time);
|
|
// } else {
|
|
// return null;
|
|
// }
|
|
// } else {
|
|
// return call_user_func(this.fileFactory, title, this);
|
|
// }
|
|
// }
|
|
//
|
|
// /**
|
|
// * Find an instance of the named file created at the specified time
|
|
// * Returns false if the file does not exist. Repositories not supporting
|
|
// * version control should return false if the time is specified.
|
|
// *
|
|
// * @param Title|String title Title Object or String
|
|
// * @param array options Associative array of options:
|
|
// * time: requested time for a specific file version, or false for the
|
|
// * current version. An image Object will be returned which was
|
|
// * created at the specified time (which may be archived or current).
|
|
// * ignoreRedirect: If true, do not follow file redirects
|
|
// * private: If true, return restricted (deleted) files if the current
|
|
// * user is allowed to view them. Otherwise, such files will not
|
|
// * be found. If a User Object, use that user instead of the current.
|
|
// * latest: If true, load from the latest available data into File objects
|
|
// * @return File|boolean False on failure
|
|
// */
|
|
// public function findFile(title, options = []) {
|
|
// title = File::normalizeTitle(title);
|
|
// if (!title) {
|
|
// return false;
|
|
// }
|
|
// if (isset(options['bypassCache'])) {
|
|
// options['latest'] = options['bypassCache']; // b/c
|
|
// }
|
|
// time = isset(options['time']) ? options['time'] : false;
|
|
// flags = !empty(options['latest']) ? File::READ_LATEST : 0;
|
|
// # First try the current version of the file to see if it precedes the timestamp
|
|
// img = this.newFile(title);
|
|
// if (!img) {
|
|
// return false;
|
|
// }
|
|
// img.load(flags);
|
|
// if (img.exists() && (!time || img.getTimestamp() == time)) {
|
|
// return img;
|
|
// }
|
|
// # Now try an old version of the file
|
|
// if (time !== false) {
|
|
// img = this.newFile(title, time);
|
|
// if (img) {
|
|
// img.load(flags);
|
|
// if (img.exists()) {
|
|
// if (!img.isDeleted(File::DELETED_FILE)) {
|
|
// return img; // always OK
|
|
// } elseif (!empty(options['private']) &&
|
|
// img.userCan(File::DELETED_FILE,
|
|
// options['private'] instanceof User ? options['private'] : null
|
|
// )
|
|
// ) {
|
|
// return img;
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// # Now try redirects
|
|
// if (!empty(options['ignoreRedirect'])) {
|
|
// return false;
|
|
// }
|
|
// redir = this.checkRedirect(title);
|
|
// if (redir && title.getNamespace() == NS_FILE) {
|
|
// img = this.newFile(redir);
|
|
// if (!img) {
|
|
// return false;
|
|
// }
|
|
// img.load(flags);
|
|
// if (img.exists()) {
|
|
// img.redirectedFrom(title.getDBkey());
|
|
//
|
|
// return img;
|
|
// }
|
|
// }
|
|
//
|
|
// return false;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Find many files at once.
|
|
// *
|
|
// * @param array items An array of titles, or an array of findFile() options with
|
|
// * the "title" option giving the title. Example:
|
|
// *
|
|
// * findItem = [ 'title' => title, 'private' => true ];
|
|
// * findBatch = [ findItem ];
|
|
// * repo.findFiles(findBatch);
|
|
// *
|
|
// * No title should appear in items twice, as the result use titles as keys
|
|
// * @param int flags Supports:
|
|
// * - FileRepo::NAME_AND_TIME_ONLY : return a (search title => (title,timestamp)) map.
|
|
// * The search title uses the input titles; the other is the final post-redirect title.
|
|
// * All titles are returned as String DB keys and the inner array is associative.
|
|
// * @return array Map of (file name => File objects) for matches
|
|
// */
|
|
// public function findFiles(array items, flags = 0) {
|
|
// result = [];
|
|
// foreach (items as item) {
|
|
// if (is_array(item)) {
|
|
// title = item['title'];
|
|
// options = item;
|
|
// unset(options['title']);
|
|
// } else {
|
|
// title = item;
|
|
// options = [];
|
|
// }
|
|
// file = this.findFile(title, options);
|
|
// if (file) {
|
|
// searchName = File::normalizeTitle(title).getDBkey(); // must be valid
|
|
// if (flags & self::NAME_AND_TIME_ONLY) {
|
|
// result[searchName] = [
|
|
// 'title' => file.getTitle().getDBkey(),
|
|
// 'timestamp' => file.getTimestamp()
|
|
// ];
|
|
// } else {
|
|
// result[searchName] = file;
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// return result;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Find an instance of the file with this key, created at the specified time
|
|
// * Returns false if the file does not exist. Repositories not supporting
|
|
// * version control should return false if the time is specified.
|
|
// *
|
|
// * @param String sha1 Base 36 SHA-1 hash
|
|
// * @param array options Option array, same as findFile().
|
|
// * @return File|boolean False on failure
|
|
// */
|
|
// public function findFileFromKey(sha1, options = []) {
|
|
// time = isset(options['time']) ? options['time'] : false;
|
|
// # First try to find a matching current version of a file...
|
|
// if (!this.fileFactoryKey) {
|
|
// return false; // find-by-sha1 not supported
|
|
// }
|
|
// img = call_user_func(this.fileFactoryKey, sha1, this, time);
|
|
// if (img && img.exists()) {
|
|
// return img;
|
|
// }
|
|
// # Now try to find a matching old version of a file...
|
|
// if (time !== false && this.oldFileFactoryKey) { // find-by-sha1 supported?
|
|
// img = call_user_func(this.oldFileFactoryKey, sha1, this, time);
|
|
// if (img && img.exists()) {
|
|
// if (!img.isDeleted(File::DELETED_FILE)) {
|
|
// return img; // always OK
|
|
// } elseif (!empty(options['private']) &&
|
|
// img.userCan(File::DELETED_FILE,
|
|
// options['private'] instanceof User ? options['private'] : null
|
|
// )
|
|
// ) {
|
|
// return img;
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// return false;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get an array or iterator of file objects for files that have a given
|
|
// * SHA-1 content hash.
|
|
// *
|
|
// * STUB
|
|
// * @param String hash SHA-1 hash
|
|
// * @return File[]
|
|
// */
|
|
// public function findBySha1(hash) {
|
|
// return [];
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get an array of arrays or iterators of file objects for files that
|
|
// * have the given SHA-1 content hashes.
|
|
// *
|
|
// * @param array hashes An array of hashes
|
|
// * @return array An Array of arrays or iterators of file objects and the hash as key
|
|
// */
|
|
// public function findBySha1s(array hashes) {
|
|
// result = [];
|
|
// foreach (hashes as hash) {
|
|
// files = this.findBySha1(hash);
|
|
// if (count(files)) {
|
|
// result[hash] = files;
|
|
// }
|
|
// }
|
|
//
|
|
// return result;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Return an array of files where the name starts with prefix.
|
|
// *
|
|
// * STUB
|
|
// * @param String prefix The prefix to search for
|
|
// * @param int limit The maximum amount of files to return
|
|
// * @return array
|
|
// */
|
|
// public function findFilesByPrefix(prefix, limit) {
|
|
// return [];
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get the URL of thumb.php
|
|
// *
|
|
// * @return String
|
|
// */
|
|
// public function getThumbScriptUrl() {
|
|
// return this.thumbScriptUrl;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Returns true if the repository can transform files via a 404 handler
|
|
// *
|
|
// * @return boolean
|
|
// */
|
|
// public function canTransformVia404() {
|
|
// return this.transformVia404;
|
|
// }
|
|
|
|
/**
|
|
* Get the name of a file from its title Object
|
|
*
|
|
* @param Title title
|
|
* @return String
|
|
*/
|
|
public byte[] getNameFromTitle(XomwTitleOld title) {
|
|
// global wgContLang;
|
|
// if (this.initialCapital != XomwNamespace::isCapitalized(NS_FILE)) {
|
|
// name = title.getUserCaseDBKey();
|
|
// if (this.initialCapital) {
|
|
// name = wgContLang.ucfirst(name);
|
|
// }
|
|
// } else {
|
|
// name = title.getDBkey();
|
|
// }
|
|
//
|
|
// return name;
|
|
return title.getDBkey();
|
|
}
|
|
//
|
|
// /**
|
|
// * Get the public zone root storage directory of the repository
|
|
// *
|
|
// * @return String
|
|
// */
|
|
// public function getRootDirectory() {
|
|
// return this.getZonePath('public');
|
|
// }
|
|
|
|
/**
|
|
* Get a relative path including trailing slash, e.g. f/fa/
|
|
* If the repo is not hashed, returns an empty String
|
|
*
|
|
* @param String name Name of file
|
|
* @return String
|
|
*/
|
|
public byte[] getHashPath(byte[] name) {
|
|
return getHashPathForLevel(name, this.hashLevels);
|
|
}
|
|
|
|
// /**
|
|
// * Get a relative path including trailing slash, e.g. f/fa/
|
|
// * If the repo is not hashed, returns an empty String
|
|
// *
|
|
// * @param String suffix Basename of file from FileRepo::storeTemp()
|
|
// * @return String
|
|
// */
|
|
// public function getTempHashPath(suffix) {
|
|
// parts = explode('!', suffix, 2); // format is <timestamp>!<name> or just <name>
|
|
// name = isset(parts[1]) ? parts[1] : suffix; // hash path is not based on timestamp
|
|
// return self::getHashPathForLevel(name, this.hashLevels);
|
|
// }
|
|
|
|
/**
|
|
* @param String name
|
|
* @param int levels
|
|
* @return String
|
|
*/
|
|
public static byte[] getHashPathForLevel(byte[] name, int levels) {
|
|
if (levels == 0) {
|
|
return Bry_.Empty;
|
|
} else {
|
|
byte[] hash = gplx.xowa.files.Xof_file_wkr_.Md5(name);
|
|
// XO.MW: assume 2
|
|
if (levels != 2) throw Err_.new_wo_type("levels must be 2", "levels", levels);
|
|
byte[] path = new byte[5];
|
|
path[0] = path[2] = hash[0];
|
|
path[3] = hash[1];
|
|
path[1] = path[4] = Byte_ascii.Slash;
|
|
return path;
|
|
}
|
|
}
|
|
|
|
// /**
|
|
// * Get the number of hash directory levels
|
|
// *
|
|
// * @return int
|
|
// */
|
|
// public function getHashLevels() {
|
|
// return this.hashLevels;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get the name of this repository, as specified by info['name]' to the constructor
|
|
// *
|
|
// * @return String
|
|
// */
|
|
// public function getName() {
|
|
// return this.name;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Make an url to this repo
|
|
// *
|
|
// * @param String query Query String to append
|
|
// * @param String entry Entry point; defaults to index
|
|
// * @return String|boolean False on failure
|
|
// */
|
|
// public function makeUrl(query = '', entry = 'index') {
|
|
// if (isset(this.scriptDirUrl)) {
|
|
// ext = isset(this.scriptExtension) ? this.scriptExtension : '.php';
|
|
//
|
|
// return wfAppendQuery("{this.scriptDirUrl}/{entry}{ext}", query);
|
|
// }
|
|
//
|
|
// return false;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get the URL of an image description page. May return false if it is
|
|
// * unknown or not applicable. In general this should only be called by the
|
|
// * File class, since it may return invalid results for certain kinds of
|
|
// * repositories. Use File::getDescriptionUrl() in user code.
|
|
// *
|
|
// * In particular, it uses the article paths as specified to the repository
|
|
// * constructor, whereas local repositories use the local Title functions.
|
|
// *
|
|
// * @param String name
|
|
// * @return String|false
|
|
// */
|
|
// public function getDescriptionUrl(name) {
|
|
// encName = wfUrlencode(name);
|
|
// if (!is_null(this.descBaseUrl)) {
|
|
// # "http://example.com/wiki/File:"
|
|
// return this.descBaseUrl . encName;
|
|
// }
|
|
// if (!is_null(this.articleUrl)) {
|
|
// # "http://example.com/wiki/1"
|
|
// # We use "Image:" as the canonical namespace for
|
|
// # compatibility across all MediaWiki versions.
|
|
// return str_replace('1',
|
|
// "Image:encName", this.articleUrl);
|
|
// }
|
|
// if (!is_null(this.scriptDirUrl)) {
|
|
// # "http://example.com/w"
|
|
// # We use "Image:" as the canonical namespace for
|
|
// # compatibility across all MediaWiki versions,
|
|
// # and just sort of hope index.php is right. ;)
|
|
// return this.makeUrl("title=Image:encName");
|
|
// }
|
|
//
|
|
// return false;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get the URL of the content-only fragment of the description page. For
|
|
// * MediaWiki this means action=render. This should only be called by the
|
|
// * repository's file class, since it may return invalid results. User code
|
|
// * should use File::getDescriptionText().
|
|
// *
|
|
// * @param String name Name of image to fetch
|
|
// * @param String lang Language to fetch it in, if any.
|
|
// * @return String|false
|
|
// */
|
|
// public function getDescriptionRenderUrl(name, lang = null) {
|
|
// query = 'action=render';
|
|
// if (!is_null(lang)) {
|
|
// query .= '&uselang=' . lang;
|
|
// }
|
|
// if (isset(this.scriptDirUrl)) {
|
|
// return this.makeUrl(
|
|
// 'title=' .
|
|
// wfUrlencode('Image:' . name) .
|
|
// "&query");
|
|
// } else {
|
|
// descUrl = this.getDescriptionUrl(name);
|
|
// if (descUrl) {
|
|
// return wfAppendQuery(descUrl, query);
|
|
// } else {
|
|
// return false;
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get the URL of the stylesheet to apply to description pages
|
|
// *
|
|
// * @return String|boolean False on failure
|
|
// */
|
|
// public function getDescriptionStylesheetUrl() {
|
|
// if (isset(this.scriptDirUrl)) {
|
|
// return this.makeUrl('title=MediaWiki:Filepage.css&' .
|
|
// wfArrayToCgi(Skin::getDynamicStylesheetQuery()));
|
|
// }
|
|
//
|
|
// return false;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Store a file to a given destination.
|
|
// *
|
|
// * @param String srcPath Source file system path, storage path, or virtual URL
|
|
// * @param String dstZone Destination zone
|
|
// * @param String dstRel Destination relative path
|
|
// * @param int flags Bitwise combination of the following flags:
|
|
// * self::OVERWRITE Overwrite an existing destination file instead of failing
|
|
// * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the
|
|
// * same contents as the source
|
|
// * self::SKIP_LOCKING Skip any file locking when doing the store
|
|
// * @return Status
|
|
// */
|
|
// public function store(srcPath, dstZone, dstRel, flags = 0) {
|
|
// this.assertWritableRepo(); // fail out if read-only
|
|
//
|
|
// status = this.storeBatch([ [ srcPath, dstZone, dstRel ] ], flags);
|
|
// if (status.successCount == 0) {
|
|
// status.setOK(false);
|
|
// }
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Store a batch of files
|
|
// *
|
|
// * @param array triplets (src, dest zone, dest rel) triplets as per store()
|
|
// * @param int flags Bitwise combination of the following flags:
|
|
// * self::OVERWRITE Overwrite an existing destination file instead of failing
|
|
// * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the
|
|
// * same contents as the source
|
|
// * self::SKIP_LOCKING Skip any file locking when doing the store
|
|
// * @throws MWException
|
|
// * @return Status
|
|
// */
|
|
// public function storeBatch(array triplets, flags = 0) {
|
|
// this.assertWritableRepo(); // fail out if read-only
|
|
//
|
|
// if (flags & self::DELETE_SOURCE) {
|
|
// throw new InvalidArgumentException("DELETE_SOURCE not supported in " . __METHOD__);
|
|
// }
|
|
//
|
|
// status = this.newGood();
|
|
// backend = this.backend; // convenience
|
|
//
|
|
// operations = [];
|
|
// // Validate each triplet and get the store operation...
|
|
// foreach (triplets as triplet) {
|
|
// list(srcPath, dstZone, dstRel) = triplet;
|
|
// wfDebug(__METHOD__
|
|
// . "(\src='srcPath', \dstZone='dstZone', \dstRel='dstRel')\n"
|
|
// );
|
|
//
|
|
// // Resolve destination path
|
|
// root = this.getZonePath(dstZone);
|
|
// if (!root) {
|
|
// throw new MWException("Invalid zone: dstZone");
|
|
// }
|
|
// if (!this.validateFilename(dstRel)) {
|
|
// throw new MWException('Validation error in dstRel');
|
|
// }
|
|
// dstPath = "root/dstRel";
|
|
// dstDir = dirname(dstPath);
|
|
// // Create destination directories for this triplet
|
|
// if (!this.initDirectory(dstDir).isOK()) {
|
|
// return this.newFatal('directorycreateerror', dstDir);
|
|
// }
|
|
//
|
|
// // Resolve source to a storage path if virtual
|
|
// srcPath = this.resolveToStoragePath(srcPath);
|
|
//
|
|
// // Get the appropriate file operation
|
|
// if (FileBackend::isStoragePath(srcPath)) {
|
|
// opName = 'copy';
|
|
// } else {
|
|
// opName = 'store';
|
|
// }
|
|
// operations[] = [
|
|
// 'op' => opName,
|
|
// 'src' => srcPath,
|
|
// 'dst' => dstPath,
|
|
// 'overwrite' => flags & self::OVERWRITE,
|
|
// 'overwriteSame' => flags & self::OVERWRITE_SAME,
|
|
// ];
|
|
// }
|
|
//
|
|
// // Execute the store operation for each triplet
|
|
// opts = [ 'force' => true ];
|
|
// if (flags & self::SKIP_LOCKING) {
|
|
// opts['nonLocking'] = true;
|
|
// }
|
|
// status.merge(backend.doOperations(operations, opts));
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Deletes a batch of files.
|
|
// * Each file can be a (zone, rel) pair, virtual url, storage path.
|
|
// * It will try to delete each file, but ignores any errors that may occur.
|
|
// *
|
|
// * @param array files List of files to delete
|
|
// * @param int flags Bitwise combination of the following flags:
|
|
// * self::SKIP_LOCKING Skip any file locking when doing the deletions
|
|
// * @return Status
|
|
// */
|
|
// public function cleanupBatch(array files, flags = 0) {
|
|
// this.assertWritableRepo(); // fail out if read-only
|
|
//
|
|
// status = this.newGood();
|
|
//
|
|
// operations = [];
|
|
// foreach (files as path) {
|
|
// if (is_array(path)) {
|
|
// // This is a pair, extract it
|
|
// list(zone, rel) = path;
|
|
// path = this.getZonePath(zone) . "/rel";
|
|
// } else {
|
|
// // Resolve source to a storage path if virtual
|
|
// path = this.resolveToStoragePath(path);
|
|
// }
|
|
// operations[] = [ 'op' => 'delete', 'src' => path ];
|
|
// }
|
|
// // Actually delete files from storage...
|
|
// opts = [ 'force' => true ];
|
|
// if (flags & self::SKIP_LOCKING) {
|
|
// opts['nonLocking'] = true;
|
|
// }
|
|
// status.merge(this.backend.doOperations(operations, opts));
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Import a file from the local file system into the repo.
|
|
// * This does no locking nor journaling and overrides existing files.
|
|
// * This function can be used to write to otherwise read-only foreign repos.
|
|
// * This is intended for copying generated thumbnails into the repo.
|
|
// *
|
|
// * @param String|FSFile src Source file system path, storage path, or virtual URL
|
|
// * @param String dst Virtual URL or storage path
|
|
// * @param array|String|null options An array consisting of a key named headers
|
|
// * listing extra headers. If a String, taken as content-disposition header.
|
|
// * (Support for array of options new in 1.23)
|
|
// * @return Status
|
|
// */
|
|
// final public function quickImport(src, dst, options = null) {
|
|
// return this.quickImportBatch([ [ src, dst, options ] ]);
|
|
// }
|
|
//
|
|
// /**
|
|
// * Purge a file from the repo. This does no locking nor journaling.
|
|
// * This function can be used to write to otherwise read-only foreign repos.
|
|
// * This is intended for purging thumbnails.
|
|
// *
|
|
// * @param String path Virtual URL or storage path
|
|
// * @return Status
|
|
// */
|
|
// final public function quickPurge(path) {
|
|
// return this.quickPurgeBatch([ path ]);
|
|
// }
|
|
//
|
|
// /**
|
|
// * Deletes a directory if empty.
|
|
// * This function can be used to write to otherwise read-only foreign repos.
|
|
// *
|
|
// * @param String dir Virtual URL (or storage path) of directory to clean
|
|
// * @return Status
|
|
// */
|
|
// public function quickCleanDir(dir) {
|
|
// status = this.newGood();
|
|
// status.merge(this.backend.clean(
|
|
// [ 'dir' => this.resolveToStoragePath(dir) ]));
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Import a batch of files from the local file system into the repo.
|
|
// * This does no locking nor journaling and overrides existing files.
|
|
// * This function can be used to write to otherwise read-only foreign repos.
|
|
// * This is intended for copying generated thumbnails into the repo.
|
|
// *
|
|
// * All path parameters may be a file system path, storage path, or virtual URL.
|
|
// * When "headers" are given they are used as HTTP headers if supported.
|
|
// *
|
|
// * @param array triples List of (source path or FSFile, destination path, disposition)
|
|
// * @return Status
|
|
// */
|
|
// public function quickImportBatch(array triples) {
|
|
// status = this.newGood();
|
|
// operations = [];
|
|
// foreach (triples as triple) {
|
|
// list(src, dst) = triple;
|
|
// if (src instanceof FSFile) {
|
|
// op = 'store';
|
|
// } else {
|
|
// src = this.resolveToStoragePath(src);
|
|
// op = FileBackend::isStoragePath(src) ? 'copy' : 'store';
|
|
// }
|
|
// dst = this.resolveToStoragePath(dst);
|
|
//
|
|
// if (!isset(triple[2])) {
|
|
// headers = [];
|
|
// } elseif (is_string(triple[2])) {
|
|
// // back-compat
|
|
// headers = [ 'Content-Disposition' => triple[2] ];
|
|
// } elseif (is_array(triple[2]) && isset(triple[2]['headers'])) {
|
|
// headers = triple[2]['headers'];
|
|
// } else {
|
|
// headers = [];
|
|
// }
|
|
//
|
|
// operations[] = [
|
|
// 'op' => op,
|
|
// 'src' => src,
|
|
// 'dst' => dst,
|
|
// 'headers' => headers
|
|
// ];
|
|
// status.merge(this.initDirectory(dirname(dst)));
|
|
// }
|
|
// status.merge(this.backend.doQuickOperations(operations));
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Purge a batch of files from the repo.
|
|
// * This function can be used to write to otherwise read-only foreign repos.
|
|
// * This does no locking nor journaling and is intended for purging thumbnails.
|
|
// *
|
|
// * @param array paths List of virtual URLs or storage paths
|
|
// * @return Status
|
|
// */
|
|
// public function quickPurgeBatch(array paths) {
|
|
// status = this.newGood();
|
|
// operations = [];
|
|
// foreach (paths as path) {
|
|
// operations[] = [
|
|
// 'op' => 'delete',
|
|
// 'src' => this.resolveToStoragePath(path),
|
|
// 'ignoreMissingSource' => true
|
|
// ];
|
|
// }
|
|
// status.merge(this.backend.doQuickOperations(operations));
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Pick a random name in the temp zone and store a file to it.
|
|
// * Returns a Status Object with the file Virtual URL in the value,
|
|
// * file can later be disposed using FileRepo::freeTemp().
|
|
// *
|
|
// * @param String originalName The super name of the file as specified
|
|
// * by the user. The file extension will be maintained.
|
|
// * @param String srcPath The current location of the file.
|
|
// * @return Status Object with the URL in the value.
|
|
// */
|
|
// public function storeTemp(originalName, srcPath) {
|
|
// this.assertWritableRepo(); // fail out if read-only
|
|
//
|
|
// date = MWTimestamp::getInstance().format('YmdHis');
|
|
// hashPath = this.getHashPath(originalName);
|
|
// dstUrlRel = hashPath . date . '!' . rawurlencode(originalName);
|
|
// virtualUrl = this.getVirtualUrl('temp') . '/' . dstUrlRel;
|
|
//
|
|
// result = this.quickImport(srcPath, virtualUrl);
|
|
// result.value = virtualUrl;
|
|
//
|
|
// return result;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Remove a temporary file or mark it for garbage collection
|
|
// *
|
|
// * @param String virtualUrl The virtual URL returned by FileRepo::storeTemp()
|
|
// * @return boolean True on success, false on failure
|
|
// */
|
|
// public function freeTemp(virtualUrl) {
|
|
// this.assertWritableRepo(); // fail out if read-only
|
|
//
|
|
// temp = this.getVirtualUrl('temp');
|
|
// if (substr(virtualUrl, 0, strlen(temp)) != temp) {
|
|
// wfDebug(__METHOD__ . ": Invalid temp virtual URL\n");
|
|
//
|
|
// return false;
|
|
// }
|
|
//
|
|
// return this.quickPurge(virtualUrl).isOK();
|
|
// }
|
|
//
|
|
// /**
|
|
// * Concatenate a list of temporary files into a target file location.
|
|
// *
|
|
// * @param array srcPaths Ordered list of source virtual URLs/storage paths
|
|
// * @param String dstPath Target file system path
|
|
// * @param int flags Bitwise combination of the following flags:
|
|
// * self::DELETE_SOURCE Delete the source files on success
|
|
// * @return Status
|
|
// */
|
|
// public function concatenate(array srcPaths, dstPath, flags = 0) {
|
|
// this.assertWritableRepo(); // fail out if read-only
|
|
//
|
|
// status = this.newGood();
|
|
//
|
|
// sources = [];
|
|
// foreach (srcPaths as srcPath) {
|
|
// // Resolve source to a storage path if virtual
|
|
// source = this.resolveToStoragePath(srcPath);
|
|
// sources[] = source; // chunk to merge
|
|
// }
|
|
//
|
|
// // Concatenate the chunks into one FS file
|
|
// params = [ 'srcs' => sources, 'dst' => dstPath ];
|
|
// status.merge(this.backend.concatenate(params));
|
|
// if (!status.isOK()) {
|
|
// return status;
|
|
// }
|
|
//
|
|
// // Delete the sources if required
|
|
// if (flags & self::DELETE_SOURCE) {
|
|
// status.merge(this.quickPurgeBatch(srcPaths));
|
|
// }
|
|
//
|
|
// // Make sure status is OK, despite any quickPurgeBatch() fatals
|
|
// status.setResult(true);
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Copy or move a file either from a storage path, virtual URL,
|
|
// * or file system path, into this repository at the specified destination location.
|
|
// *
|
|
// * Returns a Status Object. On success, the value contains "new" or
|
|
// * "archived", to indicate whether the file was new with that name.
|
|
// *
|
|
// * Options to options include:
|
|
// * - headers : name/value map of HTTP headers to use in response to GET/HEAD requests
|
|
// *
|
|
// * @param String|FSFile src The source file system path, storage path, or URL
|
|
// * @param String dstRel The destination relative path
|
|
// * @param String archiveRel The relative path where the existing file is to
|
|
// * be archived, if there is one. Relative to the public zone root.
|
|
// * @param int flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
|
|
// * that the source file should be deleted if possible
|
|
// * @param array options Optional additional parameters
|
|
// * @return Status
|
|
// */
|
|
// public function publish(
|
|
// src, dstRel, archiveRel, flags = 0, array options = []
|
|
// ) {
|
|
// this.assertWritableRepo(); // fail out if read-only
|
|
//
|
|
// status = this.publishBatch(
|
|
// [ [ src, dstRel, archiveRel, options ] ], flags);
|
|
// if (status.successCount == 0) {
|
|
// status.setOK(false);
|
|
// }
|
|
// if (isset(status.value[0])) {
|
|
// status.value = status.value[0];
|
|
// } else {
|
|
// status.value = false;
|
|
// }
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Publish a batch of files
|
|
// *
|
|
// * @param array ntuples (source, dest, archive) triplets or
|
|
// * (source, dest, archive, options) 4-tuples as per publish().
|
|
// * @param int flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
|
|
// * that the source files should be deleted if possible
|
|
// * @throws MWException
|
|
// * @return Status
|
|
// */
|
|
// public function publishBatch(array ntuples, flags = 0) {
|
|
// this.assertWritableRepo(); // fail out if read-only
|
|
//
|
|
// backend = this.backend; // convenience
|
|
// // Try creating directories
|
|
// status = this.initZones('public');
|
|
// if (!status.isOK()) {
|
|
// return status;
|
|
// }
|
|
//
|
|
// status = this.newGood([]);
|
|
//
|
|
// operations = [];
|
|
// sourceFSFilesToDelete = []; // cleanup for disk source files
|
|
// // Validate each triplet and get the store operation...
|
|
// foreach (ntuples as ntuple) {
|
|
// list(src, dstRel, archiveRel) = ntuple;
|
|
// srcPath = (src instanceof FSFile) ? src.getPath() : src;
|
|
//
|
|
// options = isset(ntuple[3]) ? ntuple[3] : [];
|
|
// // Resolve source to a storage path if virtual
|
|
// srcPath = this.resolveToStoragePath(srcPath);
|
|
// if (!this.validateFilename(dstRel)) {
|
|
// throw new MWException('Validation error in dstRel');
|
|
// }
|
|
// if (!this.validateFilename(archiveRel)) {
|
|
// throw new MWException('Validation error in archiveRel');
|
|
// }
|
|
//
|
|
// publicRoot = this.getZonePath('public');
|
|
// dstPath = "publicRoot/dstRel";
|
|
// archivePath = "publicRoot/archiveRel";
|
|
//
|
|
// dstDir = dirname(dstPath);
|
|
// archiveDir = dirname(archivePath);
|
|
// // Abort immediately on directory creation errors since they're likely to be repetitive
|
|
// if (!this.initDirectory(dstDir).isOK()) {
|
|
// return this.newFatal('directorycreateerror', dstDir);
|
|
// }
|
|
// if (!this.initDirectory(archiveDir).isOK()) {
|
|
// return this.newFatal('directorycreateerror', archiveDir);
|
|
// }
|
|
//
|
|
// // Set any desired headers to be use in GET/HEAD responses
|
|
// headers = isset(options['headers']) ? options['headers'] : [];
|
|
//
|
|
// // Archive destination file if it exists.
|
|
// // This will check if the archive file also exists and fail if does.
|
|
// // This is a sanity check to avoid data loss. On Windows and Linux,
|
|
// // copy() will overwrite, so the existence check is vulnerable to
|
|
// // race conditions unless a functioning LockManager is used.
|
|
// // LocalFile also uses SELECT FOR UPDATE for synchronization.
|
|
// operations[] = [
|
|
// 'op' => 'copy',
|
|
// 'src' => dstPath,
|
|
// 'dst' => archivePath,
|
|
// 'ignoreMissingSource' => true
|
|
// ];
|
|
//
|
|
// // Copy (or move) the source file to the destination
|
|
// if (FileBackend::isStoragePath(srcPath)) {
|
|
// if (flags & self::DELETE_SOURCE) {
|
|
// operations[] = [
|
|
// 'op' => 'move',
|
|
// 'src' => srcPath,
|
|
// 'dst' => dstPath,
|
|
// 'overwrite' => true, // replace current
|
|
// 'headers' => headers
|
|
// ];
|
|
// } else {
|
|
// operations[] = [
|
|
// 'op' => 'copy',
|
|
// 'src' => srcPath,
|
|
// 'dst' => dstPath,
|
|
// 'overwrite' => true, // replace current
|
|
// 'headers' => headers
|
|
// ];
|
|
// }
|
|
// } else { // FS source path
|
|
// operations[] = [
|
|
// 'op' => 'store',
|
|
// 'src' => src, // prefer FSFile objects
|
|
// 'dst' => dstPath,
|
|
// 'overwrite' => true, // replace current
|
|
// 'headers' => headers
|
|
// ];
|
|
// if (flags & self::DELETE_SOURCE) {
|
|
// sourceFSFilesToDelete[] = srcPath;
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// // Execute the operations for each triplet
|
|
// status.merge(backend.doOperations(operations));
|
|
// // Find out which files were archived...
|
|
// foreach (ntuples as i => ntuple) {
|
|
// list(, , archiveRel) = ntuple;
|
|
// archivePath = this.getZonePath('public') . "/archiveRel";
|
|
// if (this.fileExists(archivePath)) {
|
|
// status.value[i] = 'archived';
|
|
// } else {
|
|
// status.value[i] = 'new';
|
|
// }
|
|
// }
|
|
// // Cleanup for disk source files...
|
|
// foreach (sourceFSFilesToDelete as file) {
|
|
// MediaWiki\suppressWarnings();
|
|
// unlink(file); // FS cleanup
|
|
// MediaWiki\restoreWarnings();
|
|
// }
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Creates a directory with the appropriate zone permissions.
|
|
// * Callers are responsible for doing read-only and "writable repo" checks.
|
|
// *
|
|
// * @param String dir Virtual URL (or storage path) of directory to clean
|
|
// * @return Status
|
|
// */
|
|
// protected function initDirectory(dir) {
|
|
// path = this.resolveToStoragePath(dir);
|
|
// list(, container,) = FileBackend::splitStoragePath(path);
|
|
//
|
|
// params = [ 'dir' => path ];
|
|
// if (this.isPrivate
|
|
// || container === this.zones['deleted']['container']
|
|
// || container === this.zones['temp']['container']
|
|
// ) {
|
|
// # Take all available measures to prevent web accessibility of new deleted
|
|
// # directories, in case the user has not configured offline storage
|
|
// params = [ 'noAccess' => true, 'noListing' => true ] + params;
|
|
// }
|
|
//
|
|
// status = this.newGood();
|
|
// status.merge(this.backend.prepare(params));
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Deletes a directory if empty.
|
|
// *
|
|
// * @param String dir Virtual URL (or storage path) of directory to clean
|
|
// * @return Status
|
|
// */
|
|
// public function cleanDir(dir) {
|
|
// this.assertWritableRepo(); // fail out if read-only
|
|
//
|
|
// status = this.newGood();
|
|
// status.merge(this.backend.clean(
|
|
// [ 'dir' => this.resolveToStoragePath(dir) ]));
|
|
//
|
|
// return status;
|
|
// }
|
|
|
|
/**
|
|
* Checks existence of a a file
|
|
*
|
|
* @param String file Virtual URL (or storage path) of file to check
|
|
* @return boolean
|
|
*/
|
|
public boolean fileExists(XomwFile file) {
|
|
// result = this.fileExistsBatch(file);
|
|
//
|
|
// return result[0];
|
|
return file.exists();
|
|
}
|
|
|
|
// /**
|
|
// * Checks existence of an array of files.
|
|
// *
|
|
// * @param array files Virtual URLs (or storage paths) of files to check
|
|
// * @return array Map of files and existence flags, or false
|
|
// */
|
|
// public function fileExistsBatch(array files) {
|
|
// paths = array_map([ this, 'resolveToStoragePath' ], files);
|
|
// this.backend.preloadFileStat([ 'srcs' => paths ]);
|
|
//
|
|
// result = [];
|
|
// foreach (files as key => file) {
|
|
// path = this.resolveToStoragePath(file);
|
|
// result[key] = this.backend.fileExists([ 'src' => path ]);
|
|
// }
|
|
//
|
|
// return result;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Move a file to the deletion archive.
|
|
// * If no valid deletion archive exists, this may either delete the file
|
|
// * or throw an exception, depending on the preference of the repository
|
|
// *
|
|
// * @param mixed srcRel Relative path for the file to be deleted
|
|
// * @param mixed archiveRel Relative path for the archive location.
|
|
// * Relative to a private archive directory.
|
|
// * @return Status
|
|
// */
|
|
// public function delete(srcRel, archiveRel) {
|
|
// this.assertWritableRepo(); // fail out if read-only
|
|
//
|
|
// return this.deleteBatch([ [ srcRel, archiveRel ] ]);
|
|
// }
|
|
//
|
|
// /**
|
|
// * Move a group of files to the deletion archive.
|
|
// *
|
|
// * If no valid deletion archive is configured, this may either delete the
|
|
// * file or throw an exception, depending on the preference of the repository.
|
|
// *
|
|
// * The overwrite policy is determined by the repository -- currently LocalRepo
|
|
// * assumes a naming scheme in the deleted zone based on content hash, as
|
|
// * opposed to the public zone which is assumed to be unique.
|
|
// *
|
|
// * @param array sourceDestPairs Array of source/destination pairs. Each element
|
|
// * is a two-element array containing the source file path relative to the
|
|
// * public root in the first element, and the archive file path relative
|
|
// * to the deleted zone root in the second element.
|
|
// * @throws MWException
|
|
// * @return Status
|
|
// */
|
|
// public function deleteBatch(array sourceDestPairs) {
|
|
// this.assertWritableRepo(); // fail out if read-only
|
|
//
|
|
// // Try creating directories
|
|
// status = this.initZones([ 'public', 'deleted' ]);
|
|
// if (!status.isOK()) {
|
|
// return status;
|
|
// }
|
|
//
|
|
// status = this.newGood();
|
|
//
|
|
// backend = this.backend; // convenience
|
|
// operations = [];
|
|
// // Validate filenames and create archive directories
|
|
// foreach (sourceDestPairs as pair) {
|
|
// list(srcRel, archiveRel) = pair;
|
|
// if (!this.validateFilename(srcRel)) {
|
|
// throw new MWException(__METHOD__ . ':Validation error in srcRel');
|
|
// } elseif (!this.validateFilename(archiveRel)) {
|
|
// throw new MWException(__METHOD__ . ':Validation error in archiveRel');
|
|
// }
|
|
//
|
|
// publicRoot = this.getZonePath('public');
|
|
// srcPath = "{publicRoot}/srcRel";
|
|
//
|
|
// deletedRoot = this.getZonePath('deleted');
|
|
// archivePath = "{deletedRoot}/{archiveRel}";
|
|
// archiveDir = dirname(archivePath); // does not touch FS
|
|
//
|
|
// // Create destination directories
|
|
// if (!this.initDirectory(archiveDir).isOK()) {
|
|
// return this.newFatal('directorycreateerror', archiveDir);
|
|
// }
|
|
//
|
|
// operations[] = [
|
|
// 'op' => 'move',
|
|
// 'src' => srcPath,
|
|
// 'dst' => archivePath,
|
|
// // We may have 2+ identical files being deleted,
|
|
// // all of which will map to the same destination file
|
|
// 'overwriteSame' => true // also see bug 31792
|
|
// ];
|
|
// }
|
|
//
|
|
// // Move the files by execute the operations for each pair.
|
|
// // We're now committed to returning an OK result, which will
|
|
// // lead to the files being moved in the DB also.
|
|
// opts = [ 'force' => true ];
|
|
// status.merge(backend.doOperations(operations, opts));
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Delete files in the deleted directory if they are not referenced in the filearchive table
|
|
// *
|
|
// * STUB
|
|
// * @param array storageKeys
|
|
// */
|
|
// public function cleanupDeletedBatch(array storageKeys) {
|
|
// this.assertWritableRepo();
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get a relative path for a deletion archive key,
|
|
// * e.g. s/z/a/ for sza251lrxrc1jad41h5mgilp8nysje52.jpg
|
|
// *
|
|
// * @param String key
|
|
// * @throws MWException
|
|
// * @return String
|
|
// */
|
|
// public function getDeletedHashPath(key) {
|
|
// if (strlen(key) < 31) {
|
|
// throw new MWException("Invalid storage key 'key'.");
|
|
// }
|
|
// path = '';
|
|
// for (i = 0; i < this.deletedHashLevels; i++) {
|
|
// path .= key[i] . '/';
|
|
// }
|
|
//
|
|
// return path;
|
|
// }
|
|
//
|
|
// /**
|
|
// * If a path is a virtual URL, resolve it to a storage path.
|
|
// * Otherwise, just return the path as it is.
|
|
// *
|
|
// * @param String path
|
|
// * @return String
|
|
// * @throws MWException
|
|
// */
|
|
// protected function resolveToStoragePath(path) {
|
|
// if (this.isVirtualUrl(path)) {
|
|
// return this.resolveVirtualUrl(path);
|
|
// }
|
|
//
|
|
// return path;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get a local FS copy of a file with a given virtual URL/storage path.
|
|
// * Temporary files may be purged when the file Object falls out of scope.
|
|
// *
|
|
// * @param String virtualUrl
|
|
// * @return TempFSFile|null Returns null on failure
|
|
// */
|
|
// public function getLocalCopy(virtualUrl) {
|
|
// path = this.resolveToStoragePath(virtualUrl);
|
|
//
|
|
// return this.backend.getLocalCopy([ 'src' => path ]);
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get a local FS file with a given virtual URL/storage path.
|
|
// * The file is either an original or a copy. It should not be changed.
|
|
// * Temporary files may be purged when the file Object falls out of scope.
|
|
// *
|
|
// * @param String virtualUrl
|
|
// * @return FSFile|null Returns null on failure.
|
|
// */
|
|
// public function getLocalReference(virtualUrl) {
|
|
// path = this.resolveToStoragePath(virtualUrl);
|
|
//
|
|
// return this.backend.getLocalReference([ 'src' => path ]);
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get properties of a file with a given virtual URL/storage path.
|
|
// * Properties should ultimately be obtained via FSFile::getProps().
|
|
// *
|
|
// * @param String virtualUrl
|
|
// * @return array
|
|
// */
|
|
// public function getFileProps(virtualUrl) {
|
|
// fsFile = this.getLocalReference(virtualUrl);
|
|
// mwProps = new MWFileProps(MimeMagic::singleton());
|
|
// if (fsFile) {
|
|
// props = mwProps.getPropsFromPath(fsFile.getPath(), true);
|
|
// } else {
|
|
// props = mwProps.newPlaceholderProps();
|
|
// }
|
|
//
|
|
// return props;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get the timestamp of a file with a given virtual URL/storage path
|
|
// *
|
|
// * @param String virtualUrl
|
|
// * @return String|boolean False on failure
|
|
// */
|
|
// public function getFileTimestamp(virtualUrl) {
|
|
// path = this.resolveToStoragePath(virtualUrl);
|
|
//
|
|
// return this.backend.getFileTimestamp([ 'src' => path ]);
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get the size of a file with a given virtual URL/storage path
|
|
// *
|
|
// * @param String virtualUrl
|
|
// * @return int|boolean False on failure
|
|
// */
|
|
// public function getFileSize(virtualUrl) {
|
|
// path = this.resolveToStoragePath(virtualUrl);
|
|
//
|
|
// return this.backend.getFileSize([ 'src' => path ]);
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get the sha1 (super 36) of a file with a given virtual URL/storage path
|
|
// *
|
|
// * @param String virtualUrl
|
|
// * @return String|boolean
|
|
// */
|
|
// public function getFileSha1(virtualUrl) {
|
|
// path = this.resolveToStoragePath(virtualUrl);
|
|
//
|
|
// return this.backend.getFileSha1Base36([ 'src' => path ]);
|
|
// }
|
|
//
|
|
// /**
|
|
// * Attempt to stream a file with the given virtual URL/storage path
|
|
// *
|
|
// * @param String virtualUrl
|
|
// * @param array headers Additional HTTP headers to send on success
|
|
// * @param array optHeaders HTTP request headers (if-modified-since, range, ...)
|
|
// * @return Status
|
|
// * @since 1.27
|
|
// */
|
|
// public function streamFileWithStatus(virtualUrl, headers = [], optHeaders = []) {
|
|
// path = this.resolveToStoragePath(virtualUrl);
|
|
// params = [ 'src' => path, 'headers' => headers, 'options' => optHeaders ];
|
|
//
|
|
// status = this.newGood();
|
|
// status.merge(this.backend.streamFile(params));
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Attempt to stream a file with the given virtual URL/storage path
|
|
// *
|
|
// * @deprecated since 1.26, use streamFileWithStatus
|
|
// * @param String virtualUrl
|
|
// * @param array headers Additional HTTP headers to send on success
|
|
// * @return boolean Success
|
|
// */
|
|
// public function streamFile(virtualUrl, headers = []) {
|
|
// return this.streamFileWithStatus(virtualUrl, headers).isOK();
|
|
// }
|
|
//
|
|
// /**
|
|
// * Call a callback function for every public regular file in the repository.
|
|
// * This only acts on the current version of files, not any old versions.
|
|
// * May use either the database or the filesystem.
|
|
// *
|
|
// * @param callable callback
|
|
// * @return void
|
|
// */
|
|
// public function enumFiles(callback) {
|
|
// this.enumFilesInStorage(callback);
|
|
// }
|
|
//
|
|
// /**
|
|
// * Call a callback function for every public file in the repository.
|
|
// * May use either the database or the filesystem.
|
|
// *
|
|
// * @param callable callback
|
|
// * @return void
|
|
// */
|
|
// protected function enumFilesInStorage(callback) {
|
|
// publicRoot = this.getZonePath('public');
|
|
// numDirs = 1 << (this.hashLevels * 4);
|
|
// // Use a priori assumptions about directory structure
|
|
// // to reduce the tree height of the scanning process.
|
|
// for (flatIndex = 0; flatIndex < numDirs; flatIndex++) {
|
|
// hexString = sprintf("%0{this.hashLevels}x", flatIndex);
|
|
// path = publicRoot;
|
|
// for (hexPos = 0; hexPos < this.hashLevels; hexPos++) {
|
|
// path .= '/' . substr(hexString, 0, hexPos + 1);
|
|
// }
|
|
// iterator = this.backend.getFileList([ 'dir' => path ]);
|
|
// foreach (iterator as name) {
|
|
// // Each item returned is a public file
|
|
// call_user_func(callback, "{path}/{name}");
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// /**
|
|
// * Determine if a relative path is valid, i.e. not blank or involving directory traveral
|
|
// *
|
|
// * @param String filename
|
|
// * @return boolean
|
|
// */
|
|
// public function validateFilename(filename) {
|
|
// if (strval(filename) == '') {
|
|
// return false;
|
|
// }
|
|
//
|
|
// return FileBackend::isPathTraversalFree(filename);
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get a callback function to use for cleaning error message parameters
|
|
// *
|
|
// * @return array
|
|
// */
|
|
// function getErrorCleanupFunction() {
|
|
// switch (this.pathDisclosureProtection) {
|
|
// case 'none':
|
|
// case 'simple': // b/c
|
|
// callback = [ this, 'passThrough' ];
|
|
// break;
|
|
// default: // 'paranoid'
|
|
// callback = [ this, 'paranoidClean' ];
|
|
// }
|
|
// return callback;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Path disclosure protection function
|
|
// *
|
|
// * @param String param
|
|
// * @return String
|
|
// */
|
|
// function paranoidClean(param) {
|
|
// return '[hidden]';
|
|
// }
|
|
//
|
|
// /**
|
|
// * Path disclosure protection function
|
|
// *
|
|
// * @param String param
|
|
// * @return String
|
|
// */
|
|
// function passThrough(param) {
|
|
// return param;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Create a new fatal error
|
|
// *
|
|
// * @param String message
|
|
// * @return Status
|
|
// */
|
|
// public function newFatal(message /*, parameters...*/) {
|
|
// status = call_user_func_array([ 'Status', 'newFatal' ], func_get_args());
|
|
// status.cleanCallback = this.getErrorCleanupFunction();
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Create a new good result
|
|
// *
|
|
// * @param null|String value
|
|
// * @return Status
|
|
// */
|
|
// public function newGood(value = null) {
|
|
// status = Status::newGood(value);
|
|
// status.cleanCallback = this.getErrorCleanupFunction();
|
|
//
|
|
// return status;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Checks if there is a redirect named as title. If there is, return the
|
|
// * title Object. If not, return false.
|
|
// * STUB
|
|
// *
|
|
// * @param Title title Title of image
|
|
// * @return boolean
|
|
// */
|
|
// public function checkRedirect(Title title) {
|
|
// return false;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Invalidates image redirect cache related to that image
|
|
// * Doesn't do anything for repositories that don't support image redirects.
|
|
// *
|
|
// * STUB
|
|
// * @param Title title Title of image
|
|
// */
|
|
// public function invalidateImageRedirect(Title title) {
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get the human-readable name of the repo
|
|
// *
|
|
// * @return String
|
|
// */
|
|
// public function getDisplayName() {
|
|
// global wgSitename;
|
|
//
|
|
// if (this.isLocal()) {
|
|
// return wgSitename;
|
|
// }
|
|
//
|
|
// // 'shared-repo-name-wikimediacommons' is used when wgUseInstantCommons = true
|
|
// return wfMessageFallback('shared-repo-name-' . this.name, 'shared-repo').text();
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get the portion of the file that contains the origin file name.
|
|
// * If that name is too long, then the name "thumbnail.<ext>" will be given.
|
|
// *
|
|
// * @param String name
|
|
// * @return String
|
|
// */
|
|
// public function nameForThumb(name) {
|
|
// if (strlen(name) > this.abbrvThreshold) {
|
|
// ext = FileBackend::extensionFromPath(name);
|
|
// name = (ext == '') ? 'thumbnail' : "thumbnail.ext";
|
|
// }
|
|
//
|
|
// return name;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Returns true if this the local file repository.
|
|
// *
|
|
// * @return boolean
|
|
// */
|
|
// public function isLocal() {
|
|
// return this.getName() == 'local';
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get a key on the primary cache for this repository.
|
|
// * Returns false if the repository's cache is not accessible at this site.
|
|
// * The parameters are the parts of the key, as for wfMemcKey().
|
|
// *
|
|
// * STUB
|
|
// * @return boolean
|
|
// */
|
|
// public function getSharedCacheKey(/*...*/) {
|
|
// return false;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get a key for this repo in the local cache domain. These cache keys are
|
|
// * not shared with remote instances of the repo.
|
|
// * The parameters are the parts of the key, as for wfMemcKey().
|
|
// *
|
|
// * @return String
|
|
// */
|
|
// public function getLocalCacheKey(/*...*/) {
|
|
// args = func_get_args();
|
|
// array_unshift(args, 'filerepo', this.getName());
|
|
//
|
|
// return call_user_func_array('wfMemcKey', args);
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get a temporary private FileRepo associated with this repo.
|
|
// *
|
|
// * Files will be created in the temp zone of this repo.
|
|
// * It will have the same backend as this repo.
|
|
// *
|
|
// * @return TempFileRepo
|
|
// */
|
|
// public function getTempRepo() {
|
|
// return new TempFileRepo([
|
|
// 'name' => "{this.name}-temp",
|
|
// 'backend' => this.backend,
|
|
// 'zones' => [
|
|
// 'public' => [
|
|
// // Same place storeTemp() uses in the super repo, though
|
|
// // the path hashing is mismatched, which is annoying.
|
|
// 'container' => this.zones['temp']['container'],
|
|
// 'directory' => this.zones['temp']['directory']
|
|
// ],
|
|
// 'thumb' => [
|
|
// 'container' => this.zones['temp']['container'],
|
|
// 'directory' => this.zones['temp']['directory'] == ''
|
|
// ? 'thumb'
|
|
// : this.zones['temp']['directory'] . '/thumb'
|
|
// ],
|
|
// 'transcoded' => [
|
|
// 'container' => this.zones['temp']['container'],
|
|
// 'directory' => this.zones['temp']['directory'] == ''
|
|
// ? 'transcoded'
|
|
// : this.zones['temp']['directory'] . '/transcoded'
|
|
// ]
|
|
// ],
|
|
// 'hashLevels' => this.hashLevels, // performance
|
|
// 'isPrivate' => true // all in temp zone
|
|
// ]);
|
|
// }
|
|
//
|
|
// /**
|
|
// * Get an UploadStash associated with this repo.
|
|
// *
|
|
// * @param User user
|
|
// * @return UploadStash
|
|
// */
|
|
// public function getUploadStash(User user = null) {
|
|
// return new UploadStash(this, user);
|
|
// }
|
|
//
|
|
// /**
|
|
// * Throw an exception if this repo is read-only by design.
|
|
// * This does not and should not check getReadOnlyReason().
|
|
// *
|
|
// * @return void
|
|
// * @throws MWException
|
|
// */
|
|
// protected function assertWritableRepo() {
|
|
// }
|
|
//
|
|
// /**
|
|
// * Return information about the repository.
|
|
// *
|
|
// * @return array
|
|
// * @since 1.22
|
|
// */
|
|
// public function getInfo() {
|
|
// ret = [
|
|
// 'name' => this.getName(),
|
|
// 'displayname' => this.getDisplayName(),
|
|
// 'rootUrl' => this.getZoneUrl('public'),
|
|
// 'local' => this.isLocal(),
|
|
// ];
|
|
//
|
|
// optionalSettings = [
|
|
// 'url', 'thumbUrl', 'initialCapital', 'descBaseUrl', 'scriptDirUrl', 'articleUrl',
|
|
// 'fetchDescription', 'descriptionCacheExpiry', 'scriptExtension', 'favicon'
|
|
// ];
|
|
// foreach (optionalSettings as k) {
|
|
// if (isset(this.k)) {
|
|
// ret[k] = this.k;
|
|
// }
|
|
// }
|
|
//
|
|
// return ret;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Returns whether or not storage is SHA-1 based
|
|
// * @return boolean
|
|
// */
|
|
// public function hasSha1Storage() {
|
|
// return this.hasSha1Storage;
|
|
// }
|
|
//
|
|
// /**
|
|
// * Returns whether or not repo supports having originals SHA-1s in the thumb URLs
|
|
// * @return boolean
|
|
// */
|
|
// public function supportsSha1URLs() {
|
|
// return this.supportsSha1URLs;
|
|
// }
|
|
|
|
public static final int Zone__public = 0, Zone__thumb = 1;
|
|
}
|