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