1. Project Clover database Tue Dec 20 2016 21:24:09 CET
  2. Package org.xwiki.store.filesystem.internal

File DefaultFilesystemStoreTools.java

 

Coverage histogram

../../../../../img/srcFileCovDistChart3.png
80% of files have more coverage

Code metrics

18
59
19
1
373
202
31
0.53
3.11
19
1.63

Classes

Class Line # Actions
DefaultFilesystemStoreTools 54 59 0% 31 74
0.2291666722.9%
 

Contributing tests

This file is covered by 9 tests. .

Source view

1    /*
2    * See the NOTICE file distributed with this work for additional
3    * information regarding copyright ownership.
4    *
5    * This is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU Lesser General Public License as
7    * published by the Free Software Foundation; either version 2.1 of
8    * the License, or (at your option) any later version.
9    *
10    * This software is distributed in the hope that it will be useful,
11    * but WITHOUT ANY WARRANTY; without even the implied warranty of
12    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13    * Lesser General Public License for more details.
14    *
15    * You should have received a copy of the GNU Lesser General Public
16    * License along with this software; if not, write to the Free
17    * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18    * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
19    */
20    package org.xwiki.store.filesystem.internal;
21   
22    import java.io.File;
23    import java.util.Arrays;
24    import java.util.Date;
25    import java.util.HashMap;
26    import java.util.Map;
27    import java.util.concurrent.locks.ReadWriteLock;
28   
29    import javax.inject.Inject;
30    import javax.inject.Named;
31    import javax.inject.Singleton;
32   
33    import org.apache.commons.lang3.RandomStringUtils;
34    import org.xwiki.component.annotation.Component;
35    import org.xwiki.component.phase.Initializable;
36    import org.xwiki.context.Execution;
37    import org.xwiki.environment.Environment;
38    import org.xwiki.model.reference.DocumentReference;
39    import org.xwiki.model.reference.EntityReferenceSerializer;
40    import org.xwiki.store.locks.LockProvider;
41   
42    import com.xpn.xwiki.doc.XWikiAttachment;
43    import com.xpn.xwiki.doc.XWikiDocument;
44   
45    /**
46    * Default tools for getting files to store data in the filesystem.
47    * This should be replaced by a module which provides a secure extension of java.io.File.
48    *
49    * @version $Id: b5b59ae2d4a042c55fefd70220c6794caa69054d $
50    * @since 3.0M2
51    */
52    @Component
53    @Singleton
 
54    public class DefaultFilesystemStoreTools implements FilesystemStoreTools, Initializable
55    {
56    /**
57    * The name of the directory in the work directory where the hirearchy will be stored.
58    */
59    private static final String STORAGE_DIR_NAME = "storage";
60   
61    /**
62    * The name of the directory where document information is stored.
63    * This must have a URL illegal character in it,
64    * otherwise it will be confused if/when nested spaces are implemented.
65    */
66    private static final String DOCUMENT_DIR_NAME = "~this";
67   
68    /**
69    * The directory within each document's directory where the document's attachments are stored.
70    */
71    private static final String ATTACHMENT_DIR_NAME = "attachments";
72   
73    /**
74    * The directory within each document's directory for attachments which have been deleted.
75    */
76    private static final String DELETED_ATTACHMENT_DIR_NAME = "deleted-attachments";
77   
78    /**
79    * The part of the deleted attachment directory name after this is the date of deletion,
80    * The part before this is the URL encoded attachment filename.
81    */
82    private static final String DELETED_ATTACHMENT_NAME_SEPARATOR = "-";
83   
84    /**
85    * When a file is being saved, the original will be moved to the same name with this after it.
86    * If the save operation fails then this file will be moved back to the regular position to come as
87    * close as possible to ACID transaction handling.
88    */
89    private static final String BACKUP_FILE_SUFFIX = "~bak";
90   
91    /**
92    * When a file is being deleted, it will be renamed with this at the end of the filename in the
93    * transaction. If the transaction succeeds then the temp file will be deleted, if it fails then the
94    * temp file will be renamed back to the original filename.
95    */
96    private static final String TEMP_FILE_SUFFIX = "~tmp";
97   
98    /**
99    * Serializer used for obtaining a safe file path from a document reference.
100    */
101    @Inject
102    @Named("path")
103    private EntityReferenceSerializer<String> pathSerializer;
104   
105    @Inject
106    private FilesystemAttachmentsConfiguration config;
107   
108    /**
109    * We need to get the XWiki object in order to get the work directory.
110    */
111    @Inject
112    private Execution exec;
113   
114    /**
115    * A means of acquiring locks for attachments.
116    * Because the attachments temp files are randomly named and rename is atomic, locks are not needed.
117    * DummyLockProvider provides fake locks.
118    */
119    @Inject
120    @Named("dummy")
121    private LockProvider lockProvider;
122   
123    /**
124    * Used to get store directory.
125    */
126    @Inject
127    private Environment environment;
128   
129    /**
130    * This is the directory where all of the attachments will stored.
131    */
132    private File storageDir;
133   
134    /**
135    * Testing Constructor.
136    *
137    * @param pathSerializer an EntityReferenceSerializer for generating file paths.
138    * @param storageDir the directory to store the content in.
139    * @param lockProvider a means of getting locks for making sure
140    * only one thread accesses an attachment at a time.
141    */
 
142  9 toggle public DefaultFilesystemStoreTools(final EntityReferenceSerializer<String> pathSerializer,
143    final File storageDir,
144    final LockProvider lockProvider)
145    {
146  9 this.pathSerializer = pathSerializer;
147  9 this.storageDir = storageDir;
148  9 this.lockProvider = lockProvider;
149    }
150   
151    /**
152    * Constructor for component manager.
153    */
 
154  0 toggle public DefaultFilesystemStoreTools()
155    {
156    }
157   
 
158  0 toggle @Override
159    public void initialize()
160    {
161  0 this.storageDir = new File(this.environment.getPermanentDirectory(), STORAGE_DIR_NAME);
162  0 if (config.cleanOnStartup()) {
163  0 final File dir = this.storageDir;
164  0 new Thread(new Runnable() {
 
165  0 toggle public void run()
166    {
167  0 deleteEmptyDirs(dir, 0);
168    }
169    }).start();
170    }
171    }
172   
173    /**
174    * Delete all empty directories under the given directory.
175    * A directory which contains only empty directories is also considered an empty ditectory.
176    * This function will not delete *location* unless depth is non-zero.
177    *
178    * @param location a directory to delete.
179    * @param depth used for recursion, should always be zero.
180    * @return true if the directory existed, was empty and was deleted.
181    */
 
182  0 toggle private static boolean deleteEmptyDirs(final File location, int depth)
183    {
184  0 if (location != null && location.exists() && location.isDirectory()) {
185  0 final File[] dirs = location.listFiles();
186  0 boolean empty = true;
187  0 for (int i = 0; i < dirs.length; i++) {
188  0 if (!deleteEmptyDirs(dirs[i], depth + 1)) {
189  0 empty = false;
190    }
191    }
192  0 if (empty && depth != 0) {
193  0 location.delete();
194  0 return true;
195    }
196    }
197  0 return false;
198    }
199   
 
200  22 toggle @Override
201    public File getBackupFile(final File storageFile)
202    {
203    // We pad our file names with random alphanumeric characters so that multiple operations on the same
204    // file in the same transaction do not collide, the set of all capital and lower case letters
205    // and numbers has 62 possibilities and 62^8 = 218340105584896 between 2^47 and 2^48.
206  22 return new File(storageFile.getAbsolutePath() + BACKUP_FILE_SUFFIX + RandomStringUtils.randomAlphanumeric(8));
207    }
208   
 
209  16 toggle @Override
210    public File getTempFile(final File storageFile)
211    {
212  16 return new File(storageFile.getAbsolutePath() + TEMP_FILE_SUFFIX + RandomStringUtils.randomAlphanumeric(8));
213    }
214   
 
215  0 toggle @Override
216    public DeletedAttachmentFileProvider getDeletedAttachmentFileProvider(final XWikiAttachment attachment,
217    final Date deleteDate)
218    {
219  0 return new DefaultDeletedAttachmentFileProvider(
220    this.getDeletedAttachmentDir(attachment, deleteDate), attachment.getFilename());
221    }
222   
 
223  0 toggle @Override
224    public DeletedAttachmentFileProvider getDeletedAttachmentFileProvider(final String pathToDirectory)
225    {
226  0 final File attachDir = new File(this.storageDir, this.getStorageLocationPath());
227  0 return new DefaultDeletedAttachmentFileProvider(
228    attachDir, getFilenameFromDeletedAttachmentDirectory(attachDir));
229    }
230   
 
231  0 toggle @Override
232    public Map<String, Map<Date, DeletedAttachmentFileProvider>> deletedAttachmentsForDocument(
233    final DocumentReference docRef)
234    {
235  0 final File docDir = getDocumentDir(docRef, this.storageDir, this.pathSerializer);
236  0 final File deletedAttachmentsDir = new File(docDir, DELETED_ATTACHMENT_DIR_NAME);
237  0 final Map<String, Map<Date, DeletedAttachmentFileProvider>> out =
238    new HashMap<String, Map<Date, DeletedAttachmentFileProvider>>();
239   
240  0 if (!deletedAttachmentsDir.exists()) {
241  0 return out;
242    }
243   
244  0 for (File file : Arrays.asList(deletedAttachmentsDir.listFiles())) {
245  0 final String currentName = getFilenameFromDeletedAttachmentDirectory(file);
246  0 if (out.get(currentName) == null) {
247  0 out.put(currentName, new HashMap<Date, DeletedAttachmentFileProvider>());
248    }
249  0 out.get(currentName).put(getDeleteDateFromDeletedAttachmentDirectory(file),
250    new DefaultDeletedAttachmentFileProvider(file,
251    getFilenameFromDeletedAttachmentDirectory(file)));
252    }
253  0 return out;
254    }
255   
256    /**
257    * @param directory the location of the data for the deleted attachment.
258    * @return the name of the attachment file as extracted from the directory name.
259    */
 
260  0 toggle private static String getFilenameFromDeletedAttachmentDirectory(final File directory)
261    {
262  0 final String name = directory.getName();
263  0 final String encodedOut = name.substring(0, name.lastIndexOf(DELETED_ATTACHMENT_NAME_SEPARATOR));
264  0 return GenericFileUtils.getURLDecoded(encodedOut);
265    }
266   
267    /**
268    * @param directory the location of the data for the deleted attachment.
269    * @return the deletion date as extracted from the directory name.
270    */
 
271  0 toggle private static Date getDeleteDateFromDeletedAttachmentDirectory(final File directory)
272    {
273  0 final String name = directory.getName();
274    // no need to url decode this since it should only contain numbers 0-9.
275  0 long time = Long.parseLong(name.substring(name.lastIndexOf(DELETED_ATTACHMENT_NAME_SEPARATOR) + 1));
276  0 return new Date(time);
277    }
278   
 
279  0 toggle @Override
280    public String getStorageLocationPath()
281    {
282  0 return this.storageDir.getAbsolutePath();
283    }
284   
 
285  0 toggle @Override
286    public File getGlobalFile(final String name)
287    {
288  0 return new File(this.storageDir, "~GLOBAL_" + GenericFileUtils.getURLEncoded(name));
289    }
290   
 
291  24 toggle @Override
292    public AttachmentFileProvider getAttachmentFileProvider(final XWikiAttachment attachment)
293    {
294  24 return new DefaultAttachmentFileProvider(this.getAttachmentDir(attachment),
295    attachment.getFilename());
296    }
297   
298    /**
299    * Get the directory for storing files for an attachment.
300    * This will look like storage/xwiki/Main/WebHome/~this/attachments/file.name/
301    *
302    * @param attachment the attachment to get the directory for.
303    * @return a File representing the directory. Note: The directory may not exist.
304    */
 
305  24 toggle private File getAttachmentDir(final XWikiAttachment attachment)
306    {
307  24 final XWikiDocument doc = attachment.getDoc();
308  24 if (doc == null) {
309  0 throw new NullPointerException("Could not store attachment because it is not "
310    + "associated with a document.");
311    }
312  24 final File docDir = getDocumentDir(doc.getDocumentReference(),
313    this.storageDir,
314    this.pathSerializer);
315  24 final File attachmentsDir = new File(docDir, ATTACHMENT_DIR_NAME);
316  24 return new File(attachmentsDir, GenericFileUtils.getURLEncoded(attachment.getFilename()));
317    }
318   
319    /**
320    * Get a directory for storing the contentes of a deleted attachment.
321    * The format is <document name>/~this/deleted-attachments/<attachment name>-<delete date>/
322    * <delete date> is expressed in "unix time" so it might look like:
323    * WebHome/~this/deleted-attachments/file.txt-0123456789/
324    *
325    * @param attachment the attachment to get the file for.
326    * @param deleteDate the date the attachment was deleted.
327    * @return a directory which will be repeatable only with the same inputs.
328    */
 
329  0 toggle private File getDeletedAttachmentDir(final XWikiAttachment attachment,
330    final Date deleteDate)
331    {
332  0 final XWikiDocument doc = attachment.getDoc();
333  0 if (doc == null) {
334  0 throw new NullPointerException("Could not store deleted attachment because "
335    + "it is not attached to any document.");
336    }
337  0 final File docDir = getDocumentDir(doc.getDocumentReference(),
338    this.storageDir,
339    this.pathSerializer);
340  0 final File deletedAttachmentsDir = new File(docDir, DELETED_ATTACHMENT_DIR_NAME);
341  0 final String fileName =
342    attachment.getFilename() + DELETED_ATTACHMENT_NAME_SEPARATOR + deleteDate.getTime();
343  0 return new File(deletedAttachmentsDir, GenericFileUtils.getURLEncoded(fileName));
344    }
345   
346    /**
347    * Get the directory associated with this document.
348    * This is a path obtained from the owner document reference, where each reference segment
349    * (wiki, spaces, document name) contributes to the final path.
350    * For a document called xwiki:Main.WebHome, the directory will be:
351    * <code>(storageDir)/xwiki/Main/WebHome/~this/</code>
352    *
353    * @param docRef the DocumentReference for the document to get the directory for.
354    * @param storageDir the directory to place the directory hirearcy for attachments in.
355    * @param pathSerializer an EntityReferenceSerializer which will make a directory path from an
356    * an EntityReference.
357    * @return a file path corresponding to the attachment location; each segment in the path is
358    * URL-encoded in order to be safe.
359    */
 
360  24 toggle private static File getDocumentDir(final DocumentReference docRef,
361    final File storageDir,
362    final EntityReferenceSerializer<String> pathSerializer)
363    {
364  24 final File path = new File(storageDir, pathSerializer.serialize(docRef));
365  24 return new File(path, DOCUMENT_DIR_NAME);
366    }
367   
 
368  24 toggle @Override
369    public ReadWriteLock getLockForFile(final File toLock)
370    {
371  24 return this.lockProvider.getLock(toLock);
372    }
373    }