001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. The ASF licenses this file to You
004: * under the Apache License, Version 2.0 (the "License"); you may not
005: * 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. For additional information regarding
015: * copyright in this work, please see the NOTICE file in the top level
016: * directory of this distribution.
017: */
018:
019: package org.apache.roller.business;
020:
021: import java.io.File;
022: import java.io.FileFilter;
023: import java.io.FileInputStream;
024: import java.io.FileOutputStream;
025: import java.io.IOException;
026: import java.io.InputStream;
027: import java.io.OutputStream;
028: import java.math.BigDecimal;
029: import org.apache.commons.lang.StringUtils;
030: import org.apache.commons.logging.Log;
031: import org.apache.commons.logging.LogFactory;
032: import org.apache.roller.config.RollerConfig;
033: import org.apache.roller.config.RollerRuntimeConfig;
034: import org.apache.roller.business.FileIOException;
035: import org.apache.roller.business.FilePathException;
036: import org.apache.roller.business.FileManager;
037: import org.apache.roller.business.FileNotFoundException;
038: import org.apache.roller.pojos.WeblogResource;
039: import org.apache.roller.pojos.WebsiteData;
040: import org.apache.roller.util.RollerMessages;
041: import org.apache.roller.util.URLUtilities;
042:
043: /**
044: * Manages files uploaded to Roller weblogs.
045: *
046: * This base implementation writes resources to a filesystem.
047: */
048: public class FileManagerImpl implements FileManager {
049:
050: private static Log log = LogFactory.getLog(FileManagerImpl.class);
051:
052: private String upload_dir = null;
053:
054: /**
055: * Create file manager.
056: */
057: public FileManagerImpl() {
058: String uploaddir = RollerConfig.getProperty("uploads.dir");
059:
060: // Note: System property expansion is now handled by RollerConfig.
061:
062: if (uploaddir == null || uploaddir.trim().length() < 1)
063: uploaddir = System.getProperty("user.home")
064: + File.separator + "roller_data" + File.separator
065: + "uploads";
066:
067: if (!uploaddir.endsWith(File.separator))
068: uploaddir += File.separator;
069:
070: this .upload_dir = uploaddir.replace('/', File.separatorChar);
071: }
072:
073: /**
074: * @see org.apache.roller.model.FileManager#getFile(weblog, java.lang.String)
075: */
076: public WeblogResource getFile(WebsiteData weblog, String path)
077: throws FileNotFoundException, FilePathException {
078:
079: // get a reference to the file, checks that file exists & is readable
080: File resourceFile = this .getRealFile(weblog, path);
081:
082: // make sure file is not a directory
083: if (resourceFile.isDirectory()) {
084: throw new FilePathException("Invalid path [" + path + "], "
085: + "path is a directory.");
086: }
087:
088: // everything looks good, return resource
089: return new WeblogResourceFile(weblog, path, resourceFile);
090: }
091:
092: /**
093: * @see org.apache.roller.model.FileManager#getFiles(weblog, java.lang.String)
094: */
095: public WeblogResource[] getFiles(WebsiteData weblog, String path)
096: throws FileNotFoundException, FilePathException {
097:
098: // get a reference to the dir, checks that dir exists & is readable
099: File dirFile = this .getRealFile(weblog, path);
100:
101: // make sure path is a directory
102: if (!dirFile.isDirectory()) {
103: throw new FilePathException("Invalid path [" + path + "], "
104: + "path is not a directory.");
105: }
106:
107: // everything looks good, list contents
108: WeblogResource dir = new WeblogResourceFile(weblog, path,
109: dirFile);
110:
111: return dir.getChildren();
112: }
113:
114: /**
115: * @see org.apache.roller.model.FileManager#getDirectories(weblog)
116: */
117: public WeblogResource[] getDirectories(WebsiteData weblog)
118: throws FileNotFoundException, FilePathException {
119:
120: // get a reference to the root dir, checks that dir exists & is readable
121: File dirFile = this .getRealFile(weblog, null);
122:
123: // we only want a list of directories
124: File[] dirFiles = dirFile.listFiles(new FileFilter() {
125: public boolean accept(File f) {
126: return (f != null) ? f.isDirectory() : false;
127: }
128: });
129:
130: // convert 'em to WeblogResource objects
131: WeblogResource[] resources = new WeblogResource[dirFiles.length];
132: for (int i = 0; i < dirFiles.length; i++) {
133: String filePath = dirFiles[i].getName();
134: resources[i] = new WeblogResourceFile(weblog, filePath,
135: dirFiles[i]);
136: }
137:
138: return resources;
139: }
140:
141: /**
142: * @see org.apache.roller.model.FileManager#saveFile(weblog, java.lang.String, java.lang.String, long, java.io.InputStream)
143: */
144: public void saveFile(WebsiteData weblog, String path,
145: String contentType, long size, InputStream is)
146: throws FileNotFoundException, FilePathException,
147: FileIOException {
148:
149: String savePath = path;
150: if (path.startsWith("/")) {
151: savePath = path.substring(1);
152: }
153:
154: // make sure we are allowed to save this file
155: RollerMessages msgs = new RollerMessages();
156: if (!canSave(weblog, savePath, contentType, size, msgs)) {
157: throw new FileIOException(msgs.toString());
158: }
159:
160: // make sure uploads area exists for this weblog
161: File dirPath = this .getRealFile(weblog, null);
162: File saveFile = new File(dirPath.getAbsolutePath()
163: + File.separator + savePath);
164:
165: byte[] buffer = new byte[8192];
166: int bytesRead = 0;
167: OutputStream bos = null;
168: try {
169: bos = new FileOutputStream(saveFile);
170: while ((bytesRead = is.read(buffer, 0, 8192)) != -1) {
171: bos.write(buffer, 0, bytesRead);
172: }
173:
174: log.debug("The file has been written to ["
175: + saveFile.getAbsolutePath() + "]");
176: } catch (Exception e) {
177: throw new FileIOException("ERROR uploading file", e);
178: } finally {
179: try {
180: bos.flush();
181: bos.close();
182: } catch (Exception ignored) {
183: }
184: }
185:
186: }
187:
188: /**
189: * @see org.apache.roller.model.FileManager#createDirectory(weblog, java.lang.String)
190: */
191: public void createDirectory(WebsiteData weblog, String path)
192: throws FileNotFoundException, FilePathException,
193: FileIOException {
194:
195: // get path to weblog's uploads area
196: File weblogDir = this .getRealFile(weblog, null);
197:
198: String savePath = path;
199: if (path.startsWith("/")) {
200: savePath = path.substring(1);
201: }
202:
203: if (savePath != null && savePath.indexOf('/') != -1) {
204: throw new FilePathException("Invalid path [" + path + "], "
205: + "trying to use nested directories.");
206: }
207:
208: // now construct path to new directory
209: File dir = new File(weblogDir.getAbsolutePath()
210: + File.separator + savePath);
211:
212: // check if it already exists
213: if (dir.exists() && dir.isDirectory() && dir.canRead()) {
214: // already exists, we don't need to do anything
215: return;
216: }
217:
218: try {
219: // make sure someone isn't trying to sneek outside the uploads dir
220: if (!dir.getCanonicalPath().startsWith(
221: weblogDir.getCanonicalPath())) {
222: throw new FilePathException("Invalid path [" + path
223: + "], " + "trying to get outside uploads dir.");
224: }
225: } catch (IOException ex) {
226: // rethrow as FilePathException
227: throw new FilePathException(ex);
228: }
229:
230: // create it
231: if (!dir.mkdir()) {
232: // failed for some reason
233: throw new FileIOException(
234: "Failed to create directory ["
235: + path
236: + "], "
237: + "probably doesn't have needed parent directories.");
238: }
239: }
240:
241: /**
242: * @see org.apache.roller.model.FileManager#deleteFile(weblog, java.lang.String)
243: */
244: public void deleteFile(WebsiteData weblog, String path)
245: throws FileNotFoundException, FilePathException,
246: FileIOException {
247:
248: // get path to delete file, checks that path exists and is readable
249: File delFile = this .getRealFile(weblog, path);
250:
251: if (!delFile.delete()) {
252: throw new FileIOException("Delete failed for [" + path
253: + "], " + "possibly a non-empty directory?");
254: }
255: }
256:
257: /**
258: * @see org.apache.roller.model.FileManager#overQuota(weblog)
259: */
260: public boolean overQuota(WebsiteData weblog) {
261:
262: String maxDir = RollerRuntimeConfig
263: .getProperty("uploads.dir.maxsize");
264: String maxFile = RollerRuntimeConfig
265: .getProperty("uploads.file.maxsize");
266: BigDecimal maxDirSize = new BigDecimal(maxDir); // in megabytes
267: BigDecimal maxFileSize = new BigDecimal(maxFile); // in megabytes
268:
269: long maxDirBytes = (long) (1024000 * maxDirSize.doubleValue());
270:
271: try {
272: File uploadsDir = this .getRealFile(weblog, null);
273: long weblogDirSize = this .getDirSize(uploadsDir, true);
274:
275: return weblogDirSize > maxDirBytes;
276: } catch (Exception ex) {
277: // shouldn't ever happen, this means user's uploads dir is bad
278: // rethrow as a runtime exception
279: throw new RuntimeException(ex);
280: }
281: }
282:
283: public void release() {
284: }
285:
286: /**
287: * Determine if file can be saved given current RollerConfig settings.
288: */
289: private boolean canSave(WebsiteData weblog, String path,
290: String contentType, long size, RollerMessages messages) {
291:
292: // first check, is uploading enabled?
293: if (!RollerRuntimeConfig.getBooleanProperty("uploads.enabled")) {
294: messages.addError("error.upload.disabled");
295: return false;
296: }
297:
298: // second check, does upload exceed max size for file?
299: BigDecimal maxFileMB = new BigDecimal(RollerRuntimeConfig
300: .getProperty("uploads.file.maxsize"));
301: int maxFileBytes = (int) (1024000 * maxFileMB.doubleValue());
302: log.debug("max allowed file size = " + maxFileBytes);
303: log.debug("attempted save file size = " + size);
304: if (size > maxFileBytes) {
305: messages.addError("error.upload.filemax", maxFileMB
306: .toString());
307: return false;
308: }
309:
310: // third check, does file cause weblog to exceed quota?
311: BigDecimal maxDirMB = new BigDecimal(RollerRuntimeConfig
312: .getProperty("uploads.dir.maxsize"));
313: long maxDirBytes = (long) (1024000 * maxDirMB.doubleValue());
314: try {
315: File uploadsDir = this .getRealFile(weblog, null);
316: long userDirSize = getDirSize(uploadsDir, true);
317: if (userDirSize + size > maxDirBytes) {
318: messages.addError("error.upload.dirmax", maxDirMB
319: .toString());
320: return false;
321: }
322: } catch (Exception ex) {
323: // shouldn't ever happen, means the weblogs uploads dir is bad somehow
324: // rethrow as a runtime exception
325: throw new RuntimeException(ex);
326: }
327:
328: // fourth check, is upload type allowed?
329: String allows = RollerRuntimeConfig
330: .getProperty("uploads.types.allowed");
331: String forbids = RollerRuntimeConfig
332: .getProperty("uploads.types.forbid");
333: String[] allowFiles = StringUtils.split(StringUtils
334: .deleteWhitespace(allows), ",");
335: String[] forbidFiles = StringUtils.split(StringUtils
336: .deleteWhitespace(forbids), ",");
337: if (!checkFileType(allowFiles, forbidFiles, path, contentType)) {
338: messages.addError("error.upload.forbiddenFile", allows);
339: return false;
340: }
341:
342: // fifth check, is save path viable?
343: if (path.indexOf("/") != -1) {
344: // see if directory path exists already
345: String dirPath = path.substring(0, path.lastIndexOf("/"));
346:
347: try {
348: File parent = this .getRealFile(weblog, dirPath);
349: if (parent == null || !parent.exists()) {
350: messages.addError("error.upload.badPath");
351: }
352: } catch (Exception ex) {
353: // this is okay, just means that parent dir doesn't exist
354: messages.addError("error.upload.badPath");
355: return false;
356: }
357:
358: }
359:
360: return true;
361: }
362:
363: /**
364: * Get the size in bytes of given directory.
365: *
366: * Optionally works recursively counting subdirectories if they exist.
367: */
368: private long getDirSize(File dir, boolean recurse) {
369:
370: long size = 0;
371: if (dir.exists() && dir.isDirectory() && dir.canRead()) {
372: File[] files = dir.listFiles();
373: long dirSize = 0l;
374: for (int i = 0; i < files.length; i++) {
375: if (!files[i].isDirectory()) {
376: dirSize += files[i].length();
377: } else if (recurse) {
378: // count a subdirectory
379: dirSize += getDirSize(files[i], recurse);
380: }
381: }
382: size += dirSize;
383: }
384:
385: return size;
386: }
387:
388: /**
389: * Return true if file is allowed to be uplaoded given specified allowed and
390: * forbidden file types.
391: */
392: private boolean checkFileType(String[] allowFiles,
393: String[] forbidFiles, String fileName, String contentType) {
394:
395: // TODO: Atom Publushing Protocol figure out how to handle file
396: // allow/forbid using contentType.
397: // TEMPORARY SOLUTION: In the allow/forbid lists we will continue to
398: // allow user to specify file extensions (e.g. gif, png, jpeg) but will
399: // now also allow them to specify content-type rules (e.g. */*, image/*,
400: // text/xml, etc.).
401:
402: // if content type is invalid, reject file
403: if (contentType == null || contentType.indexOf("/") == -1) {
404: return false;
405: }
406:
407: // default to false
408: boolean allowFile = false;
409:
410: // if this person hasn't listed any allows, then assume they want
411: // to allow *all* filetypes, except those listed under forbid
412: if (allowFiles == null || allowFiles.length < 1) {
413: allowFile = true;
414: }
415:
416: // First check against what is ALLOWED
417:
418: // check file against allowed file extensions
419: if (allowFiles != null && allowFiles.length > 0) {
420: for (int y = 0; y < allowFiles.length; y++) {
421: // oops, this allowed rule is a content-type, skip it
422: if (allowFiles[y].indexOf("/") != -1)
423: continue;
424: if (fileName.toLowerCase().endsWith(
425: allowFiles[y].toLowerCase())) {
426: allowFile = true;
427: break;
428: }
429: }
430: }
431:
432: // check file against allowed contentTypes
433: if (allowFiles != null && allowFiles.length > 0) {
434: for (int y = 0; y < allowFiles.length; y++) {
435: // oops, this allowed rule is NOT a content-type, skip it
436: if (allowFiles[y].indexOf("/") == -1)
437: continue;
438: if (matchContentType(allowFiles[y], contentType)) {
439: allowFile = true;
440: break;
441: }
442: }
443: }
444:
445: // First check against what is FORBIDDEN
446:
447: // check file against forbidden file extensions, overrides any allows
448: if (forbidFiles != null && forbidFiles.length > 0) {
449: for (int x = 0; x < forbidFiles.length; x++) {
450: // oops, this forbid rule is a content-type, skip it
451: if (forbidFiles[x].indexOf("/") != -1)
452: continue;
453: if (fileName.toLowerCase().endsWith(
454: forbidFiles[x].toLowerCase())) {
455: allowFile = false;
456: break;
457: }
458: }
459: }
460:
461: // check file against forbidden contentTypes, overrides any allows
462: if (forbidFiles != null && forbidFiles.length > 0) {
463: for (int x = 0; x < forbidFiles.length; x++) {
464: // oops, this forbid rule is NOT a content-type, skip it
465: if (forbidFiles[x].indexOf("/") == -1)
466: continue;
467: if (matchContentType(forbidFiles[x], contentType)) {
468: allowFile = false;
469: break;
470: }
471: }
472: }
473:
474: return allowFile;
475: }
476:
477: /**
478: * Super simple contentType range rule matching
479: */
480: private boolean matchContentType(String rangeRule,
481: String contentType) {
482: if (rangeRule.equals("*/*"))
483: return true;
484: if (rangeRule.equals(contentType))
485: return true;
486: String ruleParts[] = rangeRule.split("/");
487: String typeParts[] = contentType.split("/");
488: if (ruleParts[0].equals(typeParts[0])
489: && ruleParts[1].equals("*"))
490: return true;
491:
492: return false;
493: }
494:
495: /**
496: * Construct the full real path to a resource in a weblog's uploads area.
497: */
498: private File getRealFile(WebsiteData weblog, String path)
499: throws FileNotFoundException, FilePathException {
500:
501: // make sure uploads area exists for this weblog
502: File weblogDir = new File(this .upload_dir + weblog.getHandle());
503: if (!weblogDir.exists()) {
504: weblogDir.mkdirs();
505: }
506:
507: // crop leading slash if it exists
508: String relPath = path;
509: if (path != null && path.startsWith("/")) {
510: relPath = path.substring(1);
511: }
512:
513: // we only allow a single level of directories right now, so make
514: // sure that the path doesn't try to go multiple levels
515: if (relPath != null
516: && (relPath.lastIndexOf('/') > relPath.indexOf('/'))) {
517: throw new FilePathException("Invalid path [" + path + "], "
518: + "trying to use nested directories.");
519: }
520:
521: // convert "/" to filesystem specific file separator
522: if (relPath != null) {
523: relPath = relPath.replace('/', File.separatorChar);
524: }
525:
526: // now form the absolute path
527: String filePath = weblogDir.getAbsolutePath();
528: if (relPath != null) {
529: filePath += File.separator + relPath;
530: }
531:
532: // make sure path exists and is readable
533: File file = new File(filePath);
534: if (!file.exists()) {
535: throw new FileNotFoundException("Invalid path [" + path
536: + "], " + "directory doesn't exist.");
537: } else if (!file.canRead()) {
538: throw new FilePathException("Invalid path [" + path + "], "
539: + "cannot read from path.");
540: }
541:
542: try {
543: // make sure someone isn't trying to sneek outside the uploads dir
544: if (!file.getCanonicalPath().startsWith(
545: weblogDir.getCanonicalPath())) {
546: throw new FilePathException("Invalid path [" + path
547: + "], " + "trying to get outside uploads dir.");
548: }
549: } catch (IOException ex) {
550: // rethrow as FilePathException
551: throw new FilePathException(ex);
552: }
553:
554: return file;
555: }
556:
557: /**
558: * A FileManagerImpl specific implementation of a WeblogResource.
559: *
560: * WeblogResources from the FileManagerImpl are backed by a java.io.File
561: * object which represents the resource on a filesystem.
562: *
563: * This class is internal to the FileManagerImpl class because there should
564: * not be any external classes which need to construct their own instances
565: * of this class.
566: */
567: class WeblogResourceFile implements WeblogResource {
568:
569: // the physical java.io.File backing this resource
570: private File resourceFile = null;
571:
572: // the relative path of the resource within the weblog's uploads area
573: private String relativePath = null;
574:
575: // the weblog the resource is attached to
576: private WebsiteData weblog = null;
577:
578: public WeblogResourceFile(WebsiteData weblog, String path,
579: File file) {
580: this .weblog = weblog;
581: relativePath = path;
582: resourceFile = file;
583: }
584:
585: public WebsiteData getWeblog() {
586: return weblog;
587: }
588:
589: public String getURL(boolean absolute) {
590: return URLUtilities.getWeblogResourceURL(weblog,
591: relativePath, absolute);
592: }
593:
594: public WeblogResource[] getChildren() {
595:
596: if (!resourceFile.isDirectory()) {
597: return null;
598: }
599:
600: // we only want files, no directories
601: File[] dirFiles = resourceFile.listFiles(new FileFilter() {
602: public boolean accept(File f) {
603: return (f != null) ? f.isFile() : false;
604: }
605: });
606:
607: // convert Files into WeblogResources
608: WeblogResource[] resources = new WeblogResource[dirFiles.length];
609: for (int i = 0; i < dirFiles.length; i++) {
610: String filePath = dirFiles[i].getName();
611: if (relativePath != null
612: && !relativePath.trim().equals("")) {
613: filePath = relativePath + "/" + filePath;
614: }
615:
616: resources[i] = new WeblogResourceFile(weblog, filePath,
617: dirFiles[i]);
618: }
619:
620: return resources;
621: }
622:
623: public String getName() {
624: return resourceFile.getName();
625: }
626:
627: public String getPath() {
628: return relativePath;
629: }
630:
631: public long getLastModified() {
632: return resourceFile.lastModified();
633: }
634:
635: public long getLength() {
636: return resourceFile.length();
637: }
638:
639: public boolean isDirectory() {
640: return resourceFile.isDirectory();
641: }
642:
643: public boolean isFile() {
644: return resourceFile.isFile();
645: }
646:
647: public InputStream getInputStream() {
648: try {
649: return new FileInputStream(resourceFile);
650: } catch (java.io.FileNotFoundException ex) {
651: // should never happen, rethrow as runtime exception
652: throw new RuntimeException(
653: "Error constructing input stream", ex);
654: }
655: }
656: }
657:
658: }
|