mass-rename-v2.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. <?php
  2. // Copyright (c) 2020 DBMXPCA Technologies. All rights reserved.
  3. // www.dbmxpca.com
  4. // Date Created: May 18, 2020
  5. // Last Updated: May 19, 2020
  6. // Version: 2.00
  7. // ====================================================================================
  8. // --- SETTINGS --- SETTINGS --- SETTINGS --- SETTINGS --- SETTINGS --- SETTINGS ---
  9. // ====================================================================================
  10. // If you are running through a large dataset that may consume a large amount
  11. // of memory, adjust this as necessary.
  12. ini_set('memory_limit', -1);
  13. // If true, the script will search for all possible occurrences of itself
  14. // and remove it from the directory traversal so as to prevent any alter-
  15. // ations to the script itself. This can be useful but for larger dir-
  16. // ectory traversals can consume a significant amount of time. It is rec-
  17. // ommended that this is enabled only for small datasets or if time is
  18. // not of significant concern.
  19. $remove_self = false;
  20. // If true, the script will show a preview of all discovered files and dir-
  21. // ectories. See the setting "$max_preview_size" for maximum preview results.
  22. $enable_dir_scan_preview = true;
  23. // If true, the script will spew the full command immediately prior to its
  24. // execution during the final renaming process.
  25. $display_full_cmd = true;
  26. // If true, the script will spew the progress during the rename operation.
  27. $display_progress = true;
  28. // If true, directories will be skipped and thus will not be renamed.
  29. $skip_directories = true;
  30. // Maximum results per preview.
  31. $max_preview_size = 100;
  32. // Number of characters to limit the full path & file command preview. This
  33. // must be a negative number in order to display the last N characters.
  34. $cmd_display_limit = -32;
  35. // ====================================================================================
  36. // +++++++++++ DO NOT EDIT BEYOND THIS POINT +++++++++++ DO NOT EDIT BEYOND THIS POINT
  37. // ====================================================================================
  38. $dirToTraverse = -1;
  39. $regex_search = "";
  40. $replace_str = "";
  41. $thisFile = __DIR__ . "/" . $argv[0];
  42. $script_time_start = 0;
  43. $script_time_end = 0;
  44. function GET_DIR_CONTENTS($dir, &$results = array()) {
  45. $files = scandir($dir);
  46. foreach ($files as $key => $value) {
  47. $path = realpath($dir . DIRECTORY_SEPARATOR . $value);
  48. if (!is_dir($path)) {
  49. $results[] = $path;
  50. } else if ($value != "." && $value != "..") {
  51. GET_DIR_CONTENTS($path, $results);
  52. $results[] = $path;
  53. }
  54. }
  55. return $results;
  56. }
  57. function ECHO_USAGE(){
  58. global $argv;
  59. echo "\n > Usage:\n";
  60. echo " \"php " . $argv[0] . " <t-dir> <search-regex> <replace-str>\"\n";
  61. echo " where <t-dir> is the directory to traverse and\n";
  62. echo " <search-regex> is the regular expression to\n";
  63. echo " search for, and <replace-str> is the raw string\n";
  64. echo " to replace the search-regex with. Note that\n";
  65. echo " <replace-str> is NOT a regular expression.\n";
  66. echo " If <replace-str> contains one or more whitespace\n";
  67. echo " character(s), it must be enclosed within double-\n";
  68. echo " quotation marks.\n";
  69. echo "\n > Examples:\n";
  70. echo " > \"php " . $argv[0] . " . /.[a-z]+/ .jpg\"\n";
  71. echo " > \"php " . $argv[0] . " . /.[a-zA-Z]+/ .jpg\"\n";
  72. echo " > \"php " . $argv[0] . " . /.[a-zA-Z0-9]+/ \".jpg\"\"\n";
  73. }
  74. // @BRIEF Replace $search with $replace in $str. This performs a simple replace using PHP's str_replace().
  75. function REPLACE($str, $search, $replace){
  76. return str_replace($search, $replace, $str);
  77. }
  78. // @BRIEF Replace $search with $replace in $str. This expects a regular expression for $search and uses PHP's preg_replace().
  79. function REPLACE2($str, $search, $replace){
  80. return preg_replace($search, $replace, $str);
  81. }
  82. // Returns true if both strings are the same. Performs a case-insensitive comparison unless third parameter is true.
  83. function ARE_STRINGS_EQUAL($str1, $str2, $case_sensitive = false){
  84. switch($case_sensitive){
  85. case true:
  86. if (strcmp($str1, $str2) == 0){
  87. return true;
  88. }
  89. else{
  90. return false;
  91. }
  92. break;
  93. default:
  94. if (strcasecmp($str1, $str2) == 0){
  95. return true;
  96. }
  97. else{
  98. return false;
  99. }
  100. break;
  101. }
  102. return false;
  103. }
  104. // ======================================================================
  105. if (isset($argv[1])){
  106. $dirToTraverse = $argv[1];
  107. echo " > Check passed; Traversal Directory = [" . $dirToTraverse . "]\n";
  108. }
  109. else{
  110. echo " > Check FAILED; Missing Traversal Directory!\n";
  111. ECHO_USAGE();
  112. echo "\n > Script terminated.\n";
  113. exit(1);
  114. }
  115. if (isset($argv[2])){
  116. $regex_search = $argv[2];
  117. echo " > Check passed; Search RegEx = \"" . $regex_search . "\"\n";
  118. }
  119. else{
  120. echo " > Check FAILED; Missing Search RegEx!\n";
  121. ECHO_USAGE();
  122. echo "\n > Script terminated.\n";
  123. exit(1);
  124. }
  125. if (isset($argv[3])){
  126. $replace_str = $argv[3];
  127. echo " > Check passed; Replacement String = \"" . $replace_str . "\"\n";
  128. }
  129. else{
  130. echo " > Check FAILED; Missing Search RegEx!\n";
  131. ECHO_USAGE();
  132. echo "\n > Script terminated.\n";
  133. exit(1);
  134. }
  135. echo " > Performing initial directory traversal, please wait...\n";
  136. $script_time_start = microtime(true);
  137. $contents = array();
  138. GET_DIR_CONTENTS($dirToTraverse, $contents);
  139. if ($remove_self){
  140. while( ($found = array_search($thisFile, $contents)) !== false ){
  141. echo " > Removing this script from file listing.\n";
  142. unset($contents[$found]);
  143. }
  144. }
  145. // var_dump($contents);
  146. $script_time_end = microtime(true);
  147. $script_runtime = $script_time_end - $script_time_start;
  148. echo " > Initial directory traversal complete [" . $script_runtime . " ms].\n";
  149. $counter = 0;
  150. $stop_preview = false;
  151. if ($enable_dir_scan_preview){
  152. echo " > ORIGINAL FILE AND DIRECTORY PREVIEW:\n ================================\n";
  153. foreach ($contents as $c){
  154. if ($stop_preview)
  155. break;
  156. $counter++;
  157. if ($counter > $max_preview_size){
  158. echo " --------- (results truncated)\n";
  159. $stop_preview = true;
  160. break;
  161. }
  162. echo " " . $c . "\n";
  163. }
  164. echo " ================================\n";
  165. }
  166. $content_map = array();
  167. $content_map_new = array();
  168. // V2 Update: Build a content map with ONLY the path, while
  169. // honoring the $skip_directory setting. The content map will
  170. // be roughly the same if $skip_directories is false but should
  171. // usually be shortened if the aforementioned setting is true.
  172. foreach ($contents as $c){
  173. switch($skip_directories){
  174. case true:
  175. if (!is_dir($c))
  176. array_push($content_map, $c);
  177. break;
  178. default:
  179. array_push($content_map, $c);
  180. break;
  181. }
  182. }
  183. // Instead of copying the entire array, we'll add to it as necessary later.
  184. // $content_map_new = $content_map;
  185. echo " > Creating new content map based on supplied search and replace settings, please wait...\n";
  186. // print_r($content_map);
  187. // Build the replacement content map. This contains the same path => filename
  188. // format but with the updated filename instead of the original.
  189. $script_time_start = microtime(true);
  190. // foreach ($content_map_new as $p => $f){
  191. // V2 Update: Build a new content map, with ONLY values that will be replaced,
  192. // in key => value pairs as old_value => new_value.
  193. foreach ($content_map as $p){
  194. // Keep track of old and new filenames.
  195. $filename = $p;
  196. $filename_new = $filename;
  197. // Perform the search & replacement.
  198. $filename_new = REPLACE2($filename_new, $regex_search, $replace_str);
  199. // If original and new filenames are different, then a replacement was made.
  200. // Add this to the new content map.
  201. if (!ARE_STRINGS_EQUAL($filename, $filename_new)){
  202. $content_map_new[$filename] = $filename_new;
  203. }
  204. }
  205. // Record number of items for use later.
  206. $total_items = count($content_map_new);
  207. // Account for zero-based indexing.
  208. if ($total_items > 0)
  209. $total_items++;
  210. $script_time_end = microtime(true);
  211. $script_runtime = $script_time_end - $script_time_start;
  212. echo " > Success: new content map created [" . $script_runtime . " ms].\n";
  213. echo " > FILE AND DIRECTORY PREVIEW:\n ================================\n";
  214. $counter = 0;
  215. $stop_preview = false;
  216. // Display preview of new content map.
  217. foreach ($content_map_new as $old => $new){
  218. if ($stop_preview)
  219. break;
  220. if (!is_dir($p)){
  221. $counter++;
  222. if ($counter > $max_preview_size){
  223. echo " --------- (results truncated)\n";
  224. $stop_preview = true;
  225. break;
  226. }
  227. echo " [before]: " . $old . "\n";
  228. echo " [after]: " . $new . "\n\n";
  229. }
  230. }
  231. echo "\n >>> A total of " . $total_items . " item(s) will be renamed. <<<\n\n";
  232. if ($total_items > 0){
  233. echo " *** PLEASE REVIEW ABOVE PREVIEW RESULTS CAREFULLY BEFORE PROCEEDING. ***\n\n";
  234. echo " - To begin operation (cannot be undone), type 'Y' and press ENTER/RETURN.\n";
  235. // echo " - To preview more results, type 'P' and press ENTER/RETURN.\n";
  236. echo " - To cancel script without making any changes, type 'N' and press ENTER/RETURN.\n";
  237. echo "\n >> Begin operation? [Y/n]: ";
  238. $response = fgets(STDIN);
  239. $begin_operation = substr($response, 0, strlen($response) - 1);
  240. if (ARE_STRINGS_EQUAL($begin_operation, "Y", true)){
  241. echo "\n > Operation started by user.\n";
  242. $countdown_timer = 5;
  243. while($countdown_timer > 0){
  244. echo " > Operation starting in " . $countdown_timer . " second(s)...\n";
  245. $countdown_timer--;
  246. sleep(1);
  247. }
  248. echo "\n > Operation in progress, please wait...\n";
  249. $items_remaining = $total_items;
  250. $curr_item = 0;
  251. $progress_val = 0;
  252. $script_time_start = microtime(true);
  253. foreach ($content_map_new as $old => $new){
  254. $curr_item++;
  255. $progress_val = $curr_item / $total_items;
  256. $progress_val *= 100;
  257. $progress_val_d = number_format((float)$progress_val, 2, '.', '');
  258. // TODO: Only perform this check if skip_directories is TRUE.
  259. if (!is_dir($p)){
  260. // Actual command to be executed.
  261. $cmd = "mv \"" . $old . "\" \"" . $new . "\"";
  262. // Truncated command to be displayed.
  263. $cmd_d = "-";
  264. // Make sure the strings are actually long enough to get truncated. If either are too short,
  265. // then just display the full command.
  266. if (strlen($old) >= -$cmd_display_limit && strlen($new) >= -$cmd_display_limit){
  267. $cmd_d = "mv \"..." . substr($old, $cmd_display_limit) . "\" \"..." . substr($new, $cmd_display_limit) . "\"";
  268. }
  269. else{
  270. $cmd_d = $cmd;
  271. }
  272. if (!$display_progress && !$display_full_cmd){ //00
  273. // print nothing
  274. }
  275. else if (!$display_progress && $display_full_cmd){ //01
  276. echo " >>> [" . $cmd_d . "]\n";
  277. }
  278. else if ($display_progress && !$display_full_cmd){ //10
  279. echo " >>> [Progress: " . $progress_val_d . "% (" . $curr_item . "/" . $total_items . ") | Items Remaining: " . $items_remaining . "]\n";
  280. }
  281. else if ($display_progress && $display_full_cmd){ //11
  282. echo " >>> [Progress: " . $progress_val_d . "% (" . $curr_item . "/" . $total_items . ") | Remaining: " . $items_remaining . "] | [" . $cmd_d . "]\n";
  283. }
  284. exec($cmd);
  285. }
  286. $items_remaining--;
  287. }
  288. $script_time_end = microtime(true);
  289. $script_runtime = $script_time_end - $script_time_start;
  290. echo " > Operation complete. [" . $script_runtime . " ms].\n";
  291. exit(0);
  292. }
  293. echo "\n > Operation cancelled by user.\n";
  294. exit(0);
  295. }
  296. else{
  297. echo "\n > Operation cancelled: there are no items to replace.\n";
  298. exit(0);
  299. }