Completed
Push — master ( 6e2d04...9d9a55 )
by
unknown
53:43 queued 36:07
created

Checksum::fopen()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 4
nop 2
dl 0
loc 30
rs 9.1288
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Ilja Neumann <[email protected]>
4
 *
5
 * @copyright Copyright (c) 2018, ownCloud GmbH
6
 * @license AGPL-3.0
7
 *
8
 * This code is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU Affero General Public License, version 3,
10
 * as published by the Free Software Foundation.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License, version 3,
18
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
19
 *
20
 */
21
namespace OC\Files\Storage\Wrapper;
22
23
use Icewind\Streams\CallbackWrapper;
24
use OC\Files\Stream\Checksum as ChecksumStream;
25
use OCP\Files\IHomeStorage;
26
use OC\Files\Utils\FileUtils;
27
28
/**
29
 * Class Checksum
30
 *
31
 * Computes checksums (default: SHA1, MD5, ADLER32) on all files under the /files path.
32
 * The resulting checksum can be retrieved by call getMetadata($path)
33
 *
34
 * If a file is read and has no checksum oc_filecache gets updated accordingly.
35
 *
36
 *
37
 * @package OC\Files\Storage\Wrapper
38
 */
39
class Checksum extends Wrapper {
40
41
	/** Format of checksum field in filecache */
42
	const CHECKSUMS_DB_FORMAT = 'SHA1:%s MD5:%s ADLER32:%s';
43
44
	const NOT_REQUIRED = 0;
45
	/** Calculate checksum on write (to be stored in oc_filecache) */
46
	const PATH_NEW_OR_UPDATED = 1;
47
	/** File needs to be checksummed on first read because it is already in cache but has no checksum */
48
	const PATH_IN_CACHE_WITHOUT_CHECKSUM = 2;
49
50
	/** @var array */
51
	private $pathsInCacheWithoutChecksum = [];
52
53
	/**
54
	 * @param string $path
55
	 * @param string $mode
56
	 * @return false|resource
57
	 */
58
	public function fopen($path, $mode) {
59
		$stream = $this->getWrapperStorage()->fopen($path, $mode);
60
		if (!\is_resource($stream) || $this->isReadWriteStream($mode)) {
61
			// don't wrap on error or mixed mode streams (could cause checksum corruption)
62
			return $stream;
63
		}
64
65
		$requirement = $this->getChecksumRequirement($path, $mode);
66
67
		if ($requirement === self::PATH_NEW_OR_UPDATED) {
68
			return \OC\Files\Stream\Checksum::wrap($stream, $path);
69
		}
70
71
		// If file is without checksum we save the path and create
72
		// a callback because we can only calculate the checksum
73
		// after the client has read the entire filestream once.
74
		// the checksum is then saved to oc_filecache for subsequent
75
		// retrieval (see onClose())
76
		if ($requirement == self::PATH_IN_CACHE_WITHOUT_CHECKSUM) {
77
			$checksumStream = \OC\Files\Stream\Checksum::wrap($stream, $path);
78
			return CallbackWrapper::wrap(
79
				$checksumStream,
80
				null,
81
				null,
82
				[$this, 'onClose']
83
			);
84
		}
85
86
		return $stream;
87
	}
88
89
	/**
90
	 * @param $mode
91
	 * @param $path
92
	 * @return int
93
	 */
94
	private function getChecksumRequirement($path, $mode) {
95
		$isNormalFile = true;
96
		if ($this->instanceOfStorage(IHomeStorage::class)) {
97
			// home storage stores files in "files"
98
			$isNormalFile = \substr($path, 0, 6) === 'files/';
99
		}
100
		$fileIsWritten = $mode !== 'r' && $mode !== 'rb';
101
102
		if ($isNormalFile && $fileIsWritten) {
103
			return self::PATH_NEW_OR_UPDATED;
104
		}
105
106
		// file could be in cache but without checksum for example
107
		// if mounted from ext. storage
108
		$cache = $this->getCache($path);
109
110
		$cacheEntry = $cache->get($path);
111
112
		// Cache entry is sometimes an array (partial) when encryption is enabled without id so
113
		// we ignore it.
114
		if ($cacheEntry && empty($cacheEntry['checksum']) && \is_object($cacheEntry)) {
115
			$this->pathsInCacheWithoutChecksum[$cacheEntry->getId()] = $path;
116
			return self::PATH_IN_CACHE_WITHOUT_CHECKSUM;
117
		}
118
119
		return self::NOT_REQUIRED;
120
	}
121
122
	/**
123
	 * @param $mode
124
	 * @return bool
125
	 */
126
	private function isReadWriteStream($mode) {
127
		return \strpos($mode, '+') !== false;
128
	}
129
130
	/**
131
	 * Callback registered in fopen
132
	 */
133
	public function onClose() {
134
		$cache = $this->getCache();
135
		foreach ($this->pathsInCacheWithoutChecksum as $cacheId => $path) {
136
			$cache->update(
137
				$cacheId,
138
				['checksum' => self::getChecksumsInDbFormat($path)]
139
			);
140
		}
141
142
		$this->pathsInCacheWithoutChecksum = [];
143
	}
144
145
	/**
146
	 * @param $path
147
	 * @return string Format like "SHA1:abc MD5:def ADLER32:ghi"
148
	 */
149
	private static function getChecksumsInDbFormat($path) {
150
		$checksums = ChecksumStream::getChecksums($path);
151
152
		if (empty($checksums)) {
153
			return '';
154
		}
155
156
		return \sprintf(
157
			self::CHECKSUMS_DB_FORMAT,
158
			$checksums['sha1'],
159
			$checksums['md5'],
160
			$checksums['adler32']
161
		);
162
	}
163
164
	/**
165
	 * @param string $path
166
	 * @param string $data
167
	 * @return bool
168
	 */
169
	public function file_put_contents($path, $data) {
170
		$memoryStream = \fopen('php://memory', 'r+');
171
		$checksumStream = \OC\Files\Stream\Checksum::wrap($memoryStream, $path);
172
173
		\fwrite($checksumStream, $data);
174
		\fclose($checksumStream);
175
176
		return $this->getWrapperStorage()->file_put_contents($path, $data);
177
	}
178
179
	/**
180
	 * @param string $path
181
	 * @return array
182
	 */
183
	public function getMetaData($path) {
184
		// Check if it is partial file. Partial file metadata are only checksums
185
		$parentMetaData = [];
186
		if (!FileUtils::isPartialFile($path)) {
187
			$parentMetaData = $this->getWrapperStorage()->getMetaData($path);
188
			// can be null if entry does not exist
189
			if ($parentMetaData === null) {
190
				return null;
191
			}
192
		}
193
		$parentMetaData['checksum'] = self::getChecksumsInDbFormat($path);
194
195
		if (!isset($parentMetaData['mimetype'])) {
196
			$parentMetaData['mimetype'] = 'application/octet-stream';
197
		}
198
199
		return $parentMetaData;
200
	}
201
}
202