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