/* * RED5 Open Source Flash Server - http://code.google.com/p/red5/ * * Copyright 2006-2012 by respective authors (see below). All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.red5.server.persistence; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import org.apache.mina.core.buffer.IoBuffer; import org.red5.io.amf.Input; import org.red5.io.amf.Output; import org.red5.io.object.Deserializer; import org.red5.server.api.persistence.IPersistable; import org.red5.server.api.scope.IScope; import org.red5.server.net.servlet.ServletUtils; import org.red5.server.so.SharedObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.Resource; import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.web.context.support.ServletContextResource; /** * Simple file-based persistence for objects. Lowers memory usage if used instead of RAM memory storage. * * @author The Red5 Project (red5@osflash.org) * @author Joachim Bauch (jojo@struktur.de) */ public class FilePersistence extends RamPersistence { /** * Logger */ private Logger log = LoggerFactory.getLogger(FilePersistence.class); /** * Files path */ private String path = "persistence"; /** * Root directory under file storage path */ private String rootDir = ""; /** * File extension for persistent objects */ private String extension = ".red5"; /** * Whether there's need to check for empty directories */ // TODO: make this configurable private boolean checkForEmptyDirectories = true; /** * Thread to serialize persistent objects. */ private FilePersistenceThread storeThread = null; /** * Create file persistence object from given resource pattern resolver * @param resolver Resource pattern resolver and loader */ public FilePersistence(ResourcePatternResolver resolver) { super(resolver); setPath(path); } /** * Create file persistence object for given scope * @param scope Scope */ public FilePersistence(IScope scope) { super(scope); setPath(path); } /** * Setter for file path. * * @param path New path */ public void setPath(String path) { log.debug("Set path: {}", path); Resource rootFile = resources.getResource(path); try { // check for existence if (!rootFile.exists()) { log.debug("Persistence directory does not exist"); if (rootFile instanceof ServletContextResource) { ServletContextResource servletResource = (ServletContextResource) rootFile; String contextPath = servletResource.getServletContext().getContextPath(); log.debug("Persistence context path: {}", contextPath); if ("/".equals(contextPath)) { contextPath = "/root"; } rootDir = String.format("%s/webapps%s/persistence", System.getProperty("red5.root"), contextPath); log.debug("Persistence directory path: {}", rootDir); File persistDir = new File(rootDir); if (!persistDir.mkdir()) { log.warn("Persistence directory creation failed"); } else { log.debug("Persistence directory access - read: {} write: {}", persistDir.canRead(), persistDir.canWrite()); } persistDir = null; } } else { rootDir = rootFile.getFile().getAbsolutePath(); } log.debug("Root dir: {} path: {}", rootDir, path); this.path = path; } catch (IOException err) { log.error("I/O exception thrown when setting file path to {}", path, err); throw new RuntimeException(err); } storeThread = FilePersistenceThread.getInstance(); } /** * Setter for extension. * * @param extension New extension. */ public void setExtension(String extension) { this.extension = extension; } /** * Return file path for persistable object * @param object Object to obtain file path for * @return Path on disk */ private String getObjectFilepath(IPersistable object) { return getObjectFilepath(object, false); } /** * Return file path for persistable object * @param object Object to obtain file path for * @param completePath Whether it full path full path sould be returned * @return Path on disk */ private String getObjectFilepath(IPersistable object, boolean completePath) { StringBuilder result = new StringBuilder(path); result.append('/'); result.append(object.getType()); result.append('/'); String objectPath = object.getPath(); log.debug("Object path: {}", objectPath); result.append(objectPath); if (!objectPath.endsWith("/")) { result.append('/'); } if (completePath) { String name = object.getName(); log.debug("Object name: {}", name); int pos = name.lastIndexOf('/'); if (pos >= 0) { result.append(name.substring(0, pos)); } } //fix up path int idx = -1; if (File.separatorChar != '/') { while ((idx = result.indexOf(File.separator)) != -1) { result.deleteCharAt(idx); result.insert(idx, '/'); } } if (log.isDebugEnabled()) { log.debug("Path step 1: {}", result.toString()); } //remove any './' if ((idx = result.indexOf("./")) != -1) { result.delete(idx, idx + 2); } if (log.isDebugEnabled()) { log.debug("Path step 2: {}", result.toString()); } //remove any '//' while ((idx = result.indexOf("//")) != -1) { result.deleteCharAt(idx); } if (log.isDebugEnabled()) { log.debug("Path step 3: {}", result.toString()); } return result.toString(); } /** {@inheritDoc} */ @Override protected String getObjectPath(String id, String name) { if (id.startsWith(path)) { id = id.substring(path.length() + 1); } return super.getObjectPath(id, name); } /** * Get filename for persistable object * @param object Persistable object * @return Name of file where given object is persisted to */ private String getObjectFilename(IPersistable object) { String path = getObjectFilepath(object); String name = object.getName(); if (name == null) { name = PERSISTENCE_NO_NAME; } return path + name + extension; } /** * Load resource with given name * @param name Resource name * @return Persistable object */ private IPersistable doLoad(String name) { return doLoad(name, null); } /** * Load resource with given name and attaches to persistable object * @param name Resource name * @param object Object to attach to * @return Persistable object */ private IPersistable doLoad(String name, IPersistable object) { IPersistable result = object; Resource data = resources.getResource(name); if (data == null || !data.exists()) { // No such file return null; } FileInputStream input; String filename; try { File fp = data.getFile(); if (fp.length() == 0) { // File is empty log.error("The file at {} is empty.", data.getFilename()); return null; } filename = fp.getAbsolutePath(); input = new FileInputStream(filename); } catch (FileNotFoundException e) { log.error("The file at {} does not exist.", data.getFilename()); return null; } catch (IOException e) { log.error("Could not load file from {}.", data.getFilename(), e); return null; } try { IoBuffer buf = IoBuffer.allocate(input.available()); try { ServletUtils.copy(input, buf.asOutputStream()); buf.flip(); Input in = new Input(buf); Deserializer deserializer = new Deserializer(); String className = deserializer.deserialize(in, String.class); if (result == null) { // we need to create the object first try { Class<?> theClass = Class.forName(className); Constructor<?> constructor = null; try { // try to create object by calling constructor with Input stream as parameter for (Class<?> interfaceClass : in.getClass().getInterfaces()) { constructor = theClass.getConstructor(new Class[] { interfaceClass }); if (constructor != null) { break; } } if (constructor == null) { throw new NoSuchMethodException(); } result = (IPersistable) constructor.newInstance(in); } catch (NoSuchMethodException err) { // no valid constructor found, use empty constructor result = (IPersistable) theClass.newInstance(); result.deserialize(in); } catch (InvocationTargetException err) { // error while invoking found constructor, use empty constructor result = (IPersistable) theClass.newInstance(); result.deserialize(in); } } catch (ClassNotFoundException cnfe) { log.error("Unknown class {}", className); return null; } catch (IllegalAccessException iae) { log.error("Illegal access", iae); return null; } catch (InstantiationException ie) { log.error("Could not instantiate class {}", className); return null; } // set object's properties result.setName(getObjectName(name)); result.setPath(getObjectPath(name, result.getName())); } else { // Initialize existing object String resultClass = result.getClass().getName(); if (!resultClass.equals(className)) { log.error("The classes differ: {} != {}", resultClass, className); return null; } result.deserialize(in); } } finally { buf.free(); buf = null; } if (result.getStore() != this) { result.setStore(this); } super.save(result); log.debug("Loaded persistent object {} from {}", result, filename); } catch (IOException e) { log.error("Could not load file at {}", filename); return null; } return result; } /** {@inheritDoc} */ @Override public IPersistable load(String name) { IPersistable result = super.load(name); if (result != null) { // Object has already been loaded return result; } return doLoad(path + '/' + name + extension); } /** {@inheritDoc} */ @Override public boolean load(IPersistable object) { if (object.isPersistent()) { // Already loaded return true; } return (doLoad(getObjectFilename(object), object) != null); } /** * Save persistable object * @param object Persistable object * @return <code>true</code> on success, <code>false</code> otherwise */ protected boolean saveObject(IPersistable object) { boolean result = true; String path = getObjectFilepath(object, true); log.debug("Path: {}", path); Resource resPath = resources.getResource(path); boolean exists = resPath.exists(); log.debug("Resource (dir) check #1 - file name: {} exists: {}", resPath.getFilename(), exists); File dir = null; try { if (!exists) { resPath = resources.getResource("classpath:" + path); exists = resPath.exists(); log.debug("Resource (dir) check #2 - file name: {} exists: {}", resPath.getFilename(), exists); if (!exists) { StringBuilder root = new StringBuilder(rootDir); //fix up path int idx = -1; if (File.separatorChar != '/') { while ((idx = root.indexOf(File.separator)) != -1) { root.deleteCharAt(idx); root.insert(idx, '/'); } } resPath = resources.getResource("file://" + root.toString() + path.substring(11)); exists = resPath.exists(); log.debug("Resource (dir) check #3 - file name: {} exists: {}", resPath.getFilename(), exists); } } dir = resPath.getFile(); if (!dir.isDirectory() && !dir.mkdirs()) { log.error("Could not create directory {}", dir.getAbsolutePath()); result = false; } } catch (IOException err) { log.error("Could not create resource file for path {}", path, err); err.printStackTrace(); result = false; } //if we made it this far and everything seems ok if (result) { // if it's a persistent SharedObject and it's empty don't write it to disk. APPSERVER-364 if (object instanceof SharedObject) { SharedObject soRef = (SharedObject) object; if (soRef.getAttributes().size() == 0) { // return true to trick the server into thinking everything is just fine :P return true; } } String filename = getObjectFilename(object); log.debug("File name: {}", filename); //strip path if (filename.indexOf('/') != -1) { filename = filename.substring(filename.lastIndexOf('/')); log.debug("New file name: {}", filename); } File file = new File(dir, filename); //Resource resFile = resources.getResource(filename); //log.debug("Resource (file) check #1 - file name: {} exists: {}", resPath.getFilename(), exists); IoBuffer buf = null; try { int initialSize = 8192; if (file.exists()) { // We likely also need the original file size when writing object initialSize += (int) file.length(); } buf = IoBuffer.allocate(initialSize); buf.setAutoExpand(true); Output out = new Output(buf); out.writeString(object.getClass().getName()); object.serialize(out); buf.flip(); FileOutputStream output = new FileOutputStream(file.getAbsolutePath()); ServletUtils.copy(buf.asInputStream(), output); output.close(); log.debug("Stored persistent object {} at {}", object, filename); } catch (IOException e) { log.error("Could not create / write file {}", filename, e); log.warn("Exception {}", e); result = false; } finally { if (buf != null) { buf.free(); buf = null; } file = null; dir = null; } } return result; } /** {@inheritDoc} */ @Override public boolean save(IPersistable object) { if (!super.save(object)) { return false; } storeThread.modified(object, this); return true; } /** * Remove empty dirs * @param base Base directory */ protected void checkRemoveEmptyDirectories(String base) { if (!checkForEmptyDirectories) { return; } String dir; Resource resFile = resources.getResource(base.substring(0, base.lastIndexOf('/'))); try { dir = resFile.getFile().getAbsolutePath(); } catch (IOException err) { return; } while (!dir.equals(rootDir)) { File fp = new File(dir); if (!fp.isDirectory()) { // This should never happen break; } if (fp.list().length != 0) { // Directory is not empty break; } if (!fp.delete()) { // Could not remove directory break; } // Move up one directory dir = fp.getParent(); } } /** {@inheritDoc} */ @Override public boolean remove(String name) { super.remove(name); String filename = path + '/' + name + extension; Resource resFile = resources.getResource(filename); if (!resFile.exists()) { // File already deleted return true; } try { boolean result = resFile.getFile().delete(); if (result) { checkRemoveEmptyDirectories(filename); } return result; } catch (IOException err) { return false; } } /** {@inheritDoc} */ @Override public boolean remove(IPersistable object) { return remove(getObjectId(object)); } /** {@inheritDoc} */ @Override public void notifyClose() { // Write any pending objects storeThread.notifyClose(this); super.notifyClose(); } }