class-qdr-tss-file-manager.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. <?php
  2. /**
  3. * File operations for the plugin.
  4. *
  5. * This class handles all file operations, including creating directories,
  6. * uploading files, chunked uploads, and deleting files.
  7. *
  8. * @package QDR_Temporary_Shared_Storage
  9. * @since 1.0.0
  10. */
  11. class QDR_TSS_File_Manager {
  12. /**
  13. * The single instance of the class.
  14. *
  15. * @var QDR_TSS_File_Manager
  16. * @since 1.0.0
  17. */
  18. protected static $_instance = null;
  19. /**
  20. * Main QDR_TSS_File_Manager Instance.
  21. *
  22. * Ensures only one instance of QDR_TSS_File_Manager is loaded or can be loaded.
  23. *
  24. * @since 1.0.0
  25. * @static
  26. * @return QDR_TSS_File_Manager - Main instance.
  27. */
  28. public static function instance() {
  29. if (is_null(self::$_instance)) {
  30. self::$_instance = new self();
  31. }
  32. return self::$_instance;
  33. }
  34. /**
  35. * QDR_TSS_File_Manager Constructor.
  36. */
  37. public function __construct() {
  38. // Initialize file system if needed
  39. $this->init_file_system();
  40. }
  41. /**
  42. * Initialize file system directories.
  43. *
  44. * @since 1.0.0
  45. * @return bool True on success, false on failure.
  46. */
  47. public function init_file_system() {
  48. // Create base upload directory if it doesn't exist
  49. if (!file_exists(QDR_TSS_UPLOADS_DIR)) {
  50. if (!wp_mkdir_p(QDR_TSS_UPLOADS_DIR)) {
  51. return false;
  52. }
  53. // Create .htaccess file to protect direct access
  54. $htaccess_content = "# Disable directory browsing\nOptions -Indexes\n\n# Deny direct access to all files\n<FilesMatch \".*\">\nOrder Allow,Deny\nDeny from all\n</FilesMatch>";
  55. file_put_contents(QDR_TSS_UPLOADS_DIR . '.htaccess', $htaccess_content);
  56. }
  57. // Create temporary directory for chunked uploads
  58. if (!file_exists(QDR_TSS_UPLOADS_DIR . 'tmp/')) {
  59. if (!wp_mkdir_p(QDR_TSS_UPLOADS_DIR . 'tmp/')) {
  60. return false;
  61. }
  62. }
  63. return true;
  64. }
  65. /**
  66. * Create a dated directory for storing files.
  67. *
  68. * @since 1.0.0
  69. * @param string $date Optional. Date string in Y-m-d format. Default is current date.
  70. * @return string|false Path to the directory or false on failure.
  71. */
  72. public function create_dated_directory($date = '') {
  73. if (empty($date)) {
  74. $date = date('Y/m/d');
  75. }
  76. $directory = QDR_TSS_UPLOADS_DIR . $date;
  77. if (!file_exists($directory)) {
  78. if (!wp_mkdir_p($directory)) {
  79. return false;
  80. }
  81. }
  82. return $directory;
  83. }
  84. /**
  85. * Initialize a chunked upload.
  86. *
  87. * @since 1.0.0
  88. * @return string|false The upload ID or false on failure.
  89. */
  90. public function init_chunked_upload() {
  91. $upload_id = uniqid();
  92. $upload_dir = QDR_TSS_UPLOADS_DIR . 'tmp/' . $upload_id;
  93. if (!wp_mkdir_p($upload_dir)) {
  94. return false;
  95. }
  96. return $upload_id;
  97. }
  98. /**
  99. * Store a chunk of a file.
  100. *
  101. * @since 1.0.0
  102. * @param string $upload_id The upload ID.
  103. * @param int $chunk_index The chunk index.
  104. * @param array $file The chunk file data from $_FILES.
  105. * @return bool True on success, false on failure.
  106. */
  107. public function store_chunk($upload_id, $chunk_index, $file) {
  108. $upload_dir = QDR_TSS_UPLOADS_DIR . 'tmp/' . $upload_id;
  109. // Check if the upload directory exists
  110. if (!file_exists($upload_dir)) {
  111. return false;
  112. }
  113. // Check if file was uploaded successfully
  114. if (!isset($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
  115. return false;
  116. }
  117. // Move uploaded chunk to temporary directory
  118. $chunk_path = $upload_dir . '/' . $chunk_index;
  119. return move_uploaded_file($file['tmp_name'], $chunk_path);
  120. }
  121. /**
  122. * Check if all chunks are present.
  123. *
  124. * @since 1.0.0
  125. * @param string $upload_id The upload ID.
  126. * @param int $total_chunks The total number of chunks.
  127. * @return bool True if all chunks are present, false otherwise.
  128. */
  129. public function check_all_chunks($upload_id, $total_chunks) {
  130. $upload_dir = QDR_TSS_UPLOADS_DIR . 'tmp/' . $upload_id;
  131. // Check if the upload directory exists
  132. if (!file_exists($upload_dir)) {
  133. return false;
  134. }
  135. // Check that all chunks are present
  136. for ($i = 0; $i < $total_chunks; $i++) {
  137. if (!file_exists($upload_dir . '/' . $i)) {
  138. return false;
  139. }
  140. }
  141. return true;
  142. }
  143. /**
  144. * Finalize a chunked upload.
  145. *
  146. * @since 1.0.0
  147. * @param string $upload_id The upload ID.
  148. * @param int $total_chunks The total number of chunks.
  149. * @param string $original_filename The original file name.
  150. * @return array|false {
  151. * @type string $file_path The path to the combined file.
  152. * @type int $file_size The size of the combined file.
  153. * } or false on failure.
  154. */
  155. public function finalize_chunked_upload($upload_id, $total_chunks, $original_filename) {
  156. $upload_dir = QDR_TSS_UPLOADS_DIR . 'tmp/' . $upload_id;
  157. // Check if the upload directory exists
  158. if (!file_exists($upload_dir)) {
  159. return false;
  160. }
  161. // Check that all chunks are present
  162. if (!$this->check_all_chunks($upload_id, $total_chunks)) {
  163. return false;
  164. }
  165. // Create final file directory
  166. $file_dir = $this->create_dated_directory();
  167. if (!$file_dir) {
  168. return false;
  169. }
  170. // Sanitize file name
  171. $original_filename = sanitize_file_name($original_filename);
  172. $file_name = md5($original_filename . time()) . '-' . $original_filename;
  173. $file_path = $file_dir . '/' . $file_name;
  174. // Combine chunks
  175. if (!$this->combine_chunks($upload_dir, $total_chunks, $file_path)) {
  176. return false;
  177. }
  178. // Clean up temporary directory
  179. $this->cleanup_chunks($upload_dir);
  180. // Get file size
  181. $file_size = filesize($file_path);
  182. return array(
  183. 'file_path' => str_replace(QDR_TSS_UPLOADS_DIR, '', $file_path),
  184. 'file_size' => $file_size
  185. );
  186. }
  187. /**
  188. * Combine chunks into a single file.
  189. *
  190. * @since 1.0.0
  191. * @access private
  192. * @param string $upload_dir The upload directory.
  193. * @param int $total_chunks The total number of chunks.
  194. * @param string $file_path The path to the combined file.
  195. * @return bool True on success, false on failure.
  196. */
  197. private function combine_chunks($upload_dir, $total_chunks, $file_path) {
  198. // Create final file
  199. $final_file = fopen($file_path, 'wb');
  200. if (!$final_file) {
  201. return false;
  202. }
  203. // Combine chunks
  204. for ($i = 0; $i < $total_chunks; $i++) {
  205. $chunk_path = $upload_dir . '/' . $i;
  206. $chunk = fopen($chunk_path, 'rb');
  207. if (!$chunk) {
  208. fclose($final_file);
  209. unlink($file_path);
  210. return false;
  211. }
  212. stream_copy_to_stream($chunk, $final_file);
  213. fclose($chunk);
  214. }
  215. fclose($final_file);
  216. return true;
  217. }
  218. /**
  219. * Clean up chunks after successful upload.
  220. *
  221. * @since 1.0.0
  222. * @access private
  223. * @param string $upload_dir The upload directory.
  224. * @return bool True on success, false on failure.
  225. */
  226. private function cleanup_chunks($upload_dir) {
  227. // Delete all chunk files
  228. $files = glob($upload_dir . '/*');
  229. foreach ($files as $file) {
  230. if (is_file($file)) {
  231. unlink($file);
  232. }
  233. }
  234. // Delete upload directory
  235. return rmdir($upload_dir);
  236. }
  237. /**
  238. * Delete a file.
  239. *
  240. * @since 1.0.0
  241. * @param string $file_path The file path relative to the upload directory.
  242. * @return bool True on success, false on failure.
  243. */
  244. public function delete_file($file_path) {
  245. $full_path = QDR_TSS_UPLOADS_DIR . $file_path;
  246. if (file_exists($full_path)) {
  247. return unlink($full_path);
  248. }
  249. return false;
  250. }
  251. /**
  252. * Get file size.
  253. *
  254. * @since 1.0.0
  255. * @param string $file_path The file path relative to the upload directory.
  256. * @return int|false The file size in bytes or false if file doesn't exist.
  257. */
  258. public function get_file_size($file_path) {
  259. $full_path = QDR_TSS_UPLOADS_DIR . $file_path;
  260. if (file_exists($full_path)) {
  261. return filesize($full_path);
  262. }
  263. return false;
  264. }
  265. /**
  266. * Clean up empty directories.
  267. *
  268. * @since 1.0.0
  269. * @param string $directory The directory to clean.
  270. * @return void
  271. */
  272. public function cleanup_empty_directories($directory = '') {
  273. if (empty($directory)) {
  274. $directory = QDR_TSS_UPLOADS_DIR;
  275. }
  276. // Skip special directories
  277. if (basename($directory) === 'tmp') {
  278. return;
  279. }
  280. // Skip if not a directory
  281. if (!is_dir($directory)) {
  282. return;
  283. }
  284. // Get all files and directories
  285. $files = scandir($directory);
  286. $files = array_diff($files, array('.', '..', '.htaccess'));
  287. // Process subdirectories first
  288. foreach ($files as $file) {
  289. $path = $directory . '/' . $file;
  290. if (is_dir($path)) {
  291. $this->cleanup_empty_directories($path);
  292. }
  293. }
  294. // Check if directory is empty now
  295. $remaining_files = scandir($directory);
  296. $remaining_files = array_diff($remaining_files, array('.', '..', '.htaccess'));
  297. // Remove directory if empty
  298. if (empty($remaining_files)) {
  299. @rmdir($directory);
  300. }
  301. }
  302. }