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