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}