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