Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.19% covered (warning)
76.19%
32 / 42
63.64% covered (warning)
63.64%
7 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
FSFile
78.05% covered (warning)
78.05%
32 / 41
63.64% covered (warning)
63.64%
7 / 11
22.82
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 exists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTimestamp
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getProps
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
4.01
 placeholderProps
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 getSha1Base36
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 extensionFromPath
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getPropsFromPath
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getSha1Base36FromPath
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Non-directory file on the file system.
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 * @ingroup FileBackend
8 */
9
10namespace Wikimedia\FileBackend\FSFile;
11
12use Wikimedia\Timestamp\ConvertibleTimestamp;
13use Wikimedia\Timestamp\TimestampFormat as TS;
14
15/**
16 * Class representing a non-directory file on the file system
17 *
18 * @ingroup FileBackend
19 */
20class FSFile {
21    /** @var string Path to file */
22    protected $path;
23
24    /** @var string File SHA-1 in base 36 */
25    protected $sha1Base36;
26
27    /**
28     * Sets up the file object
29     *
30     * @param string $path Path to temporary file on local disk
31     */
32    public function __construct( $path ) {
33        $this->path = $path;
34    }
35
36    /**
37     * Returns the file system path
38     *
39     * @return string
40     */
41    public function getPath() {
42        return $this->path;
43    }
44
45    /**
46     * Checks if the file exists
47     *
48     * @return bool
49     */
50    public function exists() {
51        return is_file( $this->path );
52    }
53
54    /**
55     * Get the file size in bytes
56     *
57     * @return int|bool
58     */
59    public function getSize() {
60        // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
61        return @filesize( $this->path );
62    }
63
64    /**
65     * Get the file's last-modified timestamp
66     *
67     * @return string|bool TS::MW timestamp or false on failure
68     */
69    public function getTimestamp() {
70        // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
71        $timestamp = @filemtime( $this->path );
72        if ( $timestamp !== false ) {
73            $timestamp = ConvertibleTimestamp::convert( TS::MW, $timestamp );
74        }
75
76        return $timestamp;
77    }
78
79    /**
80     * Get an associative array containing information about
81     * a file with the given storage path.
82     *
83     * Resulting array fields include:
84     *   - fileExists
85     *   - size (filesize in bytes)
86     *   - mime (as major/minor)
87     *   - file-mime (as major/minor)
88     *   - sha1 (in base 36)
89     *   - major_mime
90     *   - minor_mime
91     *
92     * @param string|bool $ext The file extension, or true to extract it from the filename.
93     *             Set it to false to ignore the extension. Currently unused.
94     * @return array
95     */
96    public function getProps( $ext = true ) {
97        $info = self::placeholderProps();
98        $info['fileExists'] = $this->exists();
99
100        if ( $info['fileExists'] ) {
101            $info['size'] = $this->getSize(); // bytes
102            $info['sha1'] = $this->getSha1Base36();
103
104            $mime = mime_content_type( $this->path );
105            # MIME type according to file contents
106            $info['file-mime'] = ( $mime === false ) ? 'unknown/unknown' : $mime;
107            # logical MIME type
108            $info['mime'] = $mime;
109
110            if ( str_contains( $mime, '/' ) ) {
111                [ $info['major_mime'], $info['minor_mime'] ] = explode( '/', $mime, 2 );
112            } else {
113                [ $info['major_mime'], $info['minor_mime'] ] = [ $mime, 'unknown' ];
114            }
115        }
116
117        return $info;
118    }
119
120    /**
121     * Placeholder file properties to use for files that don't exist
122     *
123     * Resulting array fields include:
124     *   - fileExists
125     *   - size (filesize in bytes)
126     *   - mime (as major/minor)
127     *   - file-mime (as major/minor)
128     *   - sha1 (in base 36)
129     *   - major_mime
130     *   - minor_mime
131     *
132     * @return array
133     */
134    public static function placeholderProps() {
135        return [
136            'fileExists' => false,
137            'size' => 0,
138            'file-mime' => null,
139            'major_mime' => null,
140            'minor_mime' => null,
141            'mime' => null,
142            'sha1' => '',
143        ];
144    }
145
146    /**
147     * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case
148     * encoding, zero padded to 31 digits.
149     *
150     * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
151     * fairly neatly.
152     *
153     * @param bool $recache
154     * @return bool|string False on failure
155     */
156    public function getSha1Base36( $recache = false ) {
157        if ( $this->sha1Base36 !== null && !$recache ) {
158            return $this->sha1Base36;
159        }
160
161        // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
162        $this->sha1Base36 = @sha1_file( $this->path );
163
164        if ( $this->sha1Base36 !== false ) {
165            $this->sha1Base36 = \Wikimedia\base_convert( $this->sha1Base36, 16, 36, 31 );
166        }
167
168        return $this->sha1Base36;
169    }
170
171    /**
172     * Get the final file extension from a file system path
173     *
174     * @param string $path
175     * @return string
176     */
177    public static function extensionFromPath( $path ) {
178        $i = strrpos( $path, '.' );
179
180        return strtolower( $i ? substr( $path, $i + 1 ) : '' );
181    }
182
183    /**
184     * Get an associative array containing information about a file in the local filesystem.
185     *
186     * @param string $path Absolute local filesystem path
187     * @param string|bool $ext The file extension, or true to extract it from the filename.
188     *   Set it to false to ignore the extension.
189     * @return array
190     */
191    public static function getPropsFromPath( $path, $ext = true ) {
192        $fsFile = new self( $path );
193
194        return $fsFile->getProps( $ext );
195    }
196
197    /**
198     * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case
199     * encoding, zero padded to 31 digits.
200     *
201     * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
202     * fairly neatly.
203     *
204     * @param string $path
205     * @return false|string False on failure
206     */
207    public static function getSha1Base36FromPath( $path ) {
208        $fsFile = new self( $path );
209
210        return $fsFile->getSha1Base36();
211    }
212}
213
214/** @deprecated class alias since 1.43 */
215class_alias( FSFile::class, 'FSFile' );