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}