001/* 002 * Copyright 2019 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.core.file; 017 018import java.io.File; 019import java.io.FileInputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.io.OutputStream; 023import java.nio.charset.StandardCharsets; 024import java.nio.file.DirectoryStream; 025import java.nio.file.Files; 026import java.nio.file.Path; 027import java.util.Enumeration; 028import java.util.HashMap; 029import java.util.Map; 030 031import org.apache.avalon.framework.component.Component; 032import org.apache.avalon.framework.logger.AbstractLogEnabled; 033import org.apache.avalon.framework.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.avalon.framework.service.Serviceable; 036import org.apache.cocoon.servlet.multipart.Part; 037import org.apache.cocoon.servlet.multipart.PartOnDisk; 038import org.apache.cocoon.servlet.multipart.RejectedPart; 039import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; 040import org.apache.commons.compress.archivers.zip.ZipFile; 041import org.apache.commons.io.FileUtils; 042import org.apache.commons.io.IOUtils; 043import org.apache.commons.io.file.PathUtils; 044import org.apache.excalibur.source.ModifiableTraversableSource; 045import org.apache.excalibur.source.Source; 046import org.apache.excalibur.source.SourceResolver; 047import org.apache.excalibur.source.SourceUtil; 048import org.apache.excalibur.source.impl.FileSource; 049 050import org.ametys.core.user.CurrentUserProvider; 051 052/** 053 * Helper for managing files and folders of a application directory such as 054 * WEB-INF/params 055 */ 056public final class FileHelper extends AbstractLogEnabled implements Component, Serviceable 057{ 058 /** The Avalon role name */ 059 public static final String ROLE = FileHelper.class.getName(); 060 061 /** The current user provider. */ 062 protected CurrentUserProvider _currentUserProvider; 063 064 private SourceResolver _srcResolver; 065 066 @Override 067 public void service(ServiceManager serviceManager) throws ServiceException 068 { 069 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 070 _srcResolver = (org.apache.excalibur.source.SourceResolver) serviceManager.lookup(org.apache.excalibur.source.SourceResolver.ROLE); 071 } 072 073 /** 074 * Saves text to given file in UTF-8 format 075 * 076 * @param fileURI the file URI. Must point to an existing file. 077 * @param text the UTF-8 file content 078 * @return A result map. 079 * @throws IOException If an error occurred while saving 080 */ 081 public Map<String, Object> saveFile(String fileURI, String text) throws IOException 082 { 083 Map<String, Object> result = new HashMap<>(); 084 085 ModifiableTraversableSource src = null; 086 try 087 { 088 src = (ModifiableTraversableSource) _srcResolver.resolveURI(fileURI); 089 090 if (!src.exists()) 091 { 092 result.put("success", false); 093 result.put("error", "unknown-file"); 094 return result; 095 } 096 097 if (src.isCollection()) 098 { 099 result.put("success", false); 100 result.put("error", "is-not-file"); 101 return result; 102 } 103 104 try (OutputStream os = src.getOutputStream()) 105 { 106 IOUtils.write(text, os, StandardCharsets.UTF_8); 107 } 108 109 if (src.getName().startsWith("messages") && src.getName().endsWith(".xml")) 110 { 111 result.put("isI18n", true); 112 } 113 } 114 finally 115 { 116 _srcResolver.release(src); 117 } 118 119 result.put("success", true); 120 return result; 121 } 122 123 /** 124 * Create a folder 125 * 126 * @param parentURI the parent URI, relative to the root 127 * @param name the name of the new folder to create 128 * @param renameIfExists true if the folder have to be renamed if the folder 129 * with same name already exits. 130 * @return The result Map with the name and uri of created folder, or a 131 * boolean "success" to false if an error occurs. 132 * @throws IOException If an error occurred adding the folder 133 */ 134 public Map<String, Object> addFolder(String parentURI, String name, boolean renameIfExists) throws IOException 135 { 136 Map<String, Object> result = new HashMap<>(); 137 138 FileSource parentDir = (FileSource) _srcResolver.resolveURI(parentURI); 139 140 if (!parentDir.isCollection()) 141 { 142 result.put("success", false); 143 result.put("error", "is-not-folder"); 144 return result; 145 } 146 147 int index = 2; 148 String folderName = name; 149 150 if (!renameIfExists && parentDir.getChild(folderName).exists()) 151 { 152 result.put("success", false); 153 result.put("error", "already-exist"); 154 return result; 155 } 156 157 while (parentDir.getChild(folderName).exists()) 158 { 159 folderName = name + " (" + index + ")"; 160 index++; 161 } 162 163 FileSource folder = (FileSource) parentDir.getChild(folderName); 164 folder.makeCollection(); 165 166 result.put("success", true); 167 result.put("name", folder.getName()); 168 result.put("uri", folder.getURI()); 169 170 return result; 171 } 172 173 /** 174 * Add or update a file 175 * 176 * @param part The file multipart to upload 177 * @param parentDir The parent directory 178 * @param mode The insertion mode: 'add-rename' or 'update' or null. 179 * @param unzip true to unzip .zip file 180 * @return the result map 181 * @throws IOException If an error occurred manipulating the file 182 */ 183 public Map<String, Object> addOrUpdateFile(Part part, FileSource parentDir, String mode, boolean unzip) throws IOException 184 { 185 Map<String, Object> result = new HashMap<>(); 186 187 if (!(part instanceof PartOnDisk)) 188 { 189 result.put("success", false); 190 if (part instanceof RejectedPart rejectedPart && rejectedPart.getMaxContentLength() == 0) 191 { 192 result.put("error", "infected"); 193 } 194 else // if (part == null || partUploaded instanceof RejectedPart) 195 { 196 result.put("error", "rejected"); 197 } 198 return result; 199 } 200 201 PartOnDisk uploadedFilePart = (PartOnDisk) part; 202 File uploadedFile = uploadedFilePart.getFile(); 203 204 String fileName = uploadedFile.getName(); 205 FileSource file = (FileSource) parentDir.getChild(fileName); 206 if (fileName.toLowerCase().endsWith(".zip") && unzip) 207 { 208 try 209 { 210 // Unzip the uploaded file 211 _unzip(parentDir, new ZipFile(uploadedFile, "cp437")); 212 213 result.put("unzip", true); 214 result.put("success", true); 215 return result; 216 } 217 catch (IOException e) 218 { 219 getLogger().error("Failed to unzip file " + uploadedFile.getPath(), e); 220 result.put("success", false); 221 result.put("error", "unzip-error"); 222 return result; 223 } 224 } 225 else if (file.exists()) 226 { 227 if ("add-rename".equals(mode)) 228 { 229 // Find a new name 230 String[] f = fileName.split("\\."); 231 int index = 1; 232 while (parentDir.getChild(fileName).exists()) 233 { 234 fileName = f[0] + "-" + (index++) + '.' + f[1]; 235 } 236 237 file = (FileSource) parentDir.getChild(fileName); 238 } 239 else if (!"update".equals(mode)) 240 { 241 result.put("success", false); 242 result.put("error", "already-exist"); 243 return result; 244 } 245 } 246 else 247 { 248 file.getFile().createNewFile(); 249 } 250 251 InputStream is = new FileInputStream(uploadedFile); 252 253 SourceUtil.copy(is, file.getOutputStream()); 254 255 result.put("name", file.getName()); 256 result.put("uri", file.getURI()); 257 result.put("success", true); 258 259 return result; 260 } 261 262 private void _unzip(FileSource destSrc, ZipFile zipFile) throws IOException 263 { 264 Enumeration<ZipArchiveEntry> entries = zipFile.getEntries(); 265 while (entries.hasMoreElements()) 266 { 267 FileSource parentCollection = destSrc; 268 269 ZipArchiveEntry zipEntry = entries.nextElement(); 270 271 String zipName = zipEntry.getName(); 272 String[] path = zipName.split("/"); 273 274 for (int i = 0; i < path.length - 1; i++) 275 { 276 String name = path[i]; 277 parentCollection = _addCollection(parentCollection, name); 278 } 279 280 String name = path[path.length - 1]; 281 if (zipEntry.isDirectory()) 282 { 283 parentCollection = _addCollection(parentCollection, name); 284 } 285 else 286 { 287 _addZipEntry(parentCollection, zipFile, zipEntry, name); 288 } 289 } 290 } 291 292 private FileSource _addCollection(FileSource collection, String name) throws IOException 293 { 294 FileSource src = (FileSource) collection.getChild(name); 295 if (!src.exists()) 296 { 297 src.makeCollection(); 298 } 299 300 return src; 301 } 302 303 private void _addZipEntry(FileSource collection, ZipFile zipFile, ZipArchiveEntry zipEntry, String fileName) throws IOException 304 { 305 FileSource fileSrc = (FileSource) collection.getChild(fileName); 306 307 try (InputStream is = zipFile.getInputStream(zipEntry)) 308 { 309 SourceUtil.copy(is, fileSrc.getOutputStream()); 310 } 311 catch (IOException e) 312 { 313 // Do nothing 314 } 315 } 316 317 /** 318 * Remove a folder or a file 319 * 320 * @param fileUri the file/folder URI 321 * @return the result map. 322 * @throws IOException If an error occurs while removing the folder/file 323 */ 324 public Map<String, Object> deleteFile(String fileUri) throws IOException 325 { 326 Map<String, Object> result = new HashMap<>(); 327 328 FileSource file = (FileSource) _srcResolver.resolveURI(fileUri); 329 330 if (file.exists()) 331 { 332 FileUtils.deleteQuietly(file.getFile()); 333 result.put("success", true); 334 } 335 else 336 { 337 result.put("success", false); 338 result.put("error", "no-exists"); 339 } 340 341 return result; 342 } 343 344 /** 345 * Delete all files corresponding to the file filter into the file tree. 346 * @param path the path to delete (can be a file or a directory) 347 * @param fileFilter the file filter to apply 348 * @param recursiveDelete if <code>true</code>, the file tree will be explored to delete files 349 * @param deleteEmptyDirs if <code>true</code>, empty dirs will be deleted 350 * @throws IOException if an error occured while exploring or deleting files 351 */ 352 public void delete(Path path, DirectoryStream.Filter<Path> fileFilter, boolean recursiveDelete, boolean deleteEmptyDirs) throws IOException 353 { 354 if (Files.isDirectory(path)) 355 { 356 if (recursiveDelete) 357 { 358 try (DirectoryStream<Path> entries = Files.newDirectoryStream(path)) 359 { 360 for (Path entry : entries) 361 { 362 delete(entry, fileFilter, recursiveDelete, deleteEmptyDirs); 363 } 364 } 365 } 366 367 if (deleteEmptyDirs && PathUtils.isEmptyDirectory(path)) 368 { 369 Files.delete(path); 370 } 371 } 372 else if (fileFilter.accept(path)) 373 { 374 Files.delete(path); 375 } 376 } 377 378 /** 379 * Rename a file or a folder 380 * 381 * @param fileUri the relative URI of the file or folder to rename 382 * @param name the new name of the file/folder 383 * @return The result Map with the name, path of the renamed file/folder, or 384 * a boolean "already-exist" is a file/folder already exists with 385 * this name. 386 * @throws IOException if an error occurs while renaming the file/folder 387 */ 388 public Map<String, Object> renameFile(String fileUri, String name) throws IOException 389 { 390 Map<String, Object> result = new HashMap<>(); 391 392 FileSource file = (FileSource) _srcResolver.resolveURI(fileUri); 393 FileSource parentDir = (FileSource) file.getParent(); 394 395 // Case sensitive exists 396 if (file.getFile().getName().equals(name) && parentDir.getChild(name).exists()) 397 { 398 result.put("success", false); 399 result.put("error", "already-exist"); 400 } 401 else 402 { 403 Source dest = _srcResolver.resolveURI(parentDir.getURI() + name); 404 file.moveTo(dest); 405 406 result.put("success", true); 407 result.put("uri", parentDir.getURI() + name); 408 result.put("name", name); 409 } 410 411 return result; 412 } 413 414 /** 415 * Tests if a file/folder with given name exists 416 * 417 * @param parentUri the parent folder URI 418 * @param name the name of the child 419 * @return true if the file exists 420 * @throws IOException if an error occurred 421 */ 422 public boolean hasChild(String parentUri, String name) throws IOException 423 { 424 FileSource currentDir = (FileSource) _srcResolver.resolveURI(parentUri); 425 return currentDir.getChild(name).exists(); 426 } 427 428 /** 429 * Copy a file or folder 430 * 431 * @param srcUri The URI of file/folder to copy 432 * @param parentTargetUri The URI of parent target file 433 * @return a result map with the name and uri of copied file in case of 434 * success. 435 * @throws IOException If an error occured manipulating the source 436 */ 437 public Map<String, Object> copySource(String srcUri, String parentTargetUri) throws IOException 438 { 439 Map<String, Object> result = new HashMap<>(); 440 441 FileSource srcFile = (FileSource) _srcResolver.resolveURI(srcUri); 442 443 if (!srcFile.exists()) 444 { 445 result.put("success", false); 446 result.put("error", "no-exists"); 447 return result; 448 } 449 450 String srcFileName = srcFile.getName(); 451 FileSource targetFile = (FileSource) _srcResolver.resolveURI(parentTargetUri + (srcFileName.length() > 0 ? "/" + srcFileName : "")); 452 453 // Find unique file name 454 int index = 2; 455 String fileName = srcFileName; 456 while (targetFile.exists()) 457 { 458 fileName = srcFileName + " (" + index + ")"; 459 targetFile = (FileSource) _srcResolver.resolveURI(parentTargetUri + (fileName.length() > 0 ? "/" + fileName : "")); 460 index++; 461 } 462 463 if (srcFile.getFile().isDirectory()) 464 { 465 FileUtils.copyDirectory(srcFile.getFile(), targetFile.getFile()); 466 } 467 else 468 { 469 FileUtils.copyFile(srcFile.getFile(), targetFile.getFile()); 470 } 471 472 result.put("success", true); 473 result.put("name", targetFile.getName()); 474 result.put("uri", targetFile.getURI()); 475 476 return result; 477 } 478 479 /** 480 * Move a file or folder 481 * 482 * @param srcUri The URI of file/folder to move 483 * @param parentTargetUri The URI of parent target file 484 * @return a result map with the name and uri of moved file in case of 485 * success. 486 * @throws IOException If an error occurred manipulating the source 487 */ 488 public Map<String, Object> moveSource(String srcUri, String parentTargetUri) throws IOException 489 { 490 Map<String, Object> result = new HashMap<>(); 491 492 FileSource srcFile = (FileSource) _srcResolver.resolveURI(srcUri); 493 494 if (!srcFile.exists()) 495 { 496 result.put("success", false); 497 result.put("error", "no-exists"); 498 return result; 499 } 500 501 FileSource parentDargetDir = (FileSource) _srcResolver.resolveURI(parentTargetUri); 502 String fileName = srcFile.getName(); 503 FileSource targetFile = (FileSource) _srcResolver.resolveURI(parentTargetUri + (fileName.length() > 0 ? "/" + fileName : "")); 504 505 if (targetFile.exists()) 506 { 507 result.put("msg", "already-exists"); 508 return result; 509 } 510 511 FileUtils.moveToDirectory(srcFile.getFile(), parentDargetDir.getFile(), false); 512 513 result.put("success", true); 514 result.put("name", targetFile.getName()); 515 result.put("uri", targetFile.getURI()); 516 517 return result; 518 } 519}