| /* |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you 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.apache.wiki.providers; |
| |
| import org.apache.log4j.Logger; |
| import org.apache.wiki.api.core.Attachment; |
| import org.apache.wiki.api.core.Engine; |
| import org.apache.wiki.api.core.Page; |
| import org.apache.wiki.api.exceptions.NoRequiredPropertyException; |
| import org.apache.wiki.api.exceptions.ProviderException; |
| import org.apache.wiki.api.providers.AttachmentProvider; |
| import org.apache.wiki.api.providers.WikiProvider; |
| import org.apache.wiki.api.search.QueryItem; |
| import org.apache.wiki.api.spi.Wiki; |
| import org.apache.wiki.pages.PageTimeComparator; |
| import org.apache.wiki.util.FileUtil; |
| import org.apache.wiki.util.TextUtil; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.FilenameFilter; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Properties; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Provides basic, versioning attachments. |
| * |
| * <PRE> |
| * Structure is as follows: |
| * attachment_dir/ |
| * ThisPage/ |
| * attachment.doc/ |
| * attachment.properties |
| * 1.doc |
| * 2.doc |
| * 3.doc |
| * picture.png/ |
| * attachment.properties |
| * 1.png |
| * 2.png |
| * ThatPage/ |
| * picture.png/ |
| * attachment.properties |
| * 1.png |
| * |
| * </PRE> |
| * |
| * The names of the directories will be URLencoded. |
| * <p> |
| * "attachment.properties" consists of the following items: |
| * <UL> |
| * <LI>1.author = author name for version 1 (etc) |
| * </UL> |
| */ |
| public class BasicAttachmentProvider implements AttachmentProvider { |
| |
| private Engine m_engine; |
| private String m_storageDir; |
| |
| /* |
| * Disable client cache for files with patterns |
| * since 2.5.96 |
| */ |
| private Pattern m_disableCache = null; |
| |
| /** The property name for specifying which attachments are not cached. Value is <tt>{@value}</tt>. */ |
| public static final String PROP_DISABLECACHE = "jspwiki.basicAttachmentProvider.disableCache"; |
| |
| /** The name of the property file. */ |
| public static final String PROPERTY_FILE = "attachment.properties"; |
| |
| /** The default extension for the page attachment directory name. */ |
| public static final String DIR_EXTENSION = "-att"; |
| |
| /** The default extension for the attachment directory. */ |
| public static final String ATTDIR_EXTENSION = "-dir"; |
| |
| private static final Logger log = Logger.getLogger( BasicAttachmentProvider.class ); |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void initialize( final Engine engine, final Properties properties ) throws NoRequiredPropertyException, IOException { |
| m_engine = engine; |
| m_storageDir = TextUtil.getCanonicalFilePathProperty( properties, PROP_STORAGEDIR, |
| System.getProperty("user.home") + File.separator + "jspwiki-files"); |
| |
| final String patternString = engine.getWikiProperties().getProperty( PROP_DISABLECACHE ); |
| if ( patternString != null ) { |
| m_disableCache = Pattern.compile(patternString); |
| } |
| |
| // Check if the directory exists - if it doesn't, create it. |
| final File f = new File( m_storageDir ); |
| if( !f.exists() ) { |
| f.mkdirs(); |
| } |
| |
| // Some sanity checks |
| if( !f.exists() ) { |
| throw new IOException( "Could not find or create attachment storage directory '" + m_storageDir + "'" ); |
| } |
| |
| if( !f.canWrite() ) { |
| throw new IOException( "Cannot write to the attachment storage directory '" + m_storageDir + "'" ); |
| } |
| |
| if( !f.isDirectory() ) { |
| throw new IOException( "Your attachment storage points to a file, not a directory: '" + m_storageDir + "'" ); |
| } |
| } |
| |
| /** |
| * Finds storage dir, and if it exists, makes sure that it is valid. |
| * |
| * @param wikipage Page to which this attachment is attached. |
| */ |
| private File findPageDir( String wikipage ) throws ProviderException { |
| wikipage = mangleName( wikipage ); |
| |
| final File f = new File( m_storageDir, wikipage + DIR_EXTENSION ); |
| if( f.exists() && !f.isDirectory() ) { |
| throw new ProviderException( "Storage dir '" + f.getAbsolutePath() + "' is not a directory!" ); |
| } |
| |
| return f; |
| } |
| |
| private static String mangleName( final String wikiname ) { |
| return TextUtil.urlEncodeUTF8( wikiname ); |
| } |
| |
| private static String unmangleName( final String filename ) |
| { |
| return TextUtil.urlDecodeUTF8( filename ); |
| } |
| |
| /** |
| * Finds the dir in which the attachment lives. |
| */ |
| private File findAttachmentDir( final Attachment att ) throws ProviderException { |
| File f = new File( findPageDir( att.getParentName() ), mangleName( att.getFileName() + ATTDIR_EXTENSION ) ); |
| |
| // Migration code for earlier versions of JSPWiki. Originally, we used plain filename. Then we realized we need |
| // to urlencode it. Then we realized that we have to use a postfix to make sure illegal file names are never formed. |
| if( !f.exists() ) { |
| File oldf = new File( findPageDir( att.getParentName() ), mangleName( att.getFileName() ) ); |
| if( oldf.exists() ) { |
| f = oldf; |
| } else { |
| oldf = new File( findPageDir( att.getParentName() ), att.getFileName() ); |
| if( oldf.exists() ) { |
| f = oldf; |
| } |
| } |
| } |
| |
| return f; |
| } |
| |
| /** |
| * Goes through the repository and decides which version is the newest one in that directory. |
| * |
| * @return Latest version number in the repository, or 0, if there is no page in the repository. |
| */ |
| private int findLatestVersion( final Attachment att ) throws ProviderException { |
| final File attDir = findAttachmentDir( att ); |
| final String[] pages = attDir.list( new AttachmentVersionFilter() ); |
| if( pages == null ) { |
| return 0; // No such thing found. |
| } |
| |
| int version = 0; |
| for( final String page : pages ) { |
| final int cutpoint = page.indexOf( '.' ); |
| final String pageNum = ( cutpoint > 0 ) ? page.substring( 0, cutpoint ) : page; |
| |
| try { |
| final int res = Integer.parseInt( pageNum ); |
| |
| if( res > version ) { |
| version = res; |
| } |
| } catch( final NumberFormatException e ) { |
| } // It's okay to skip these. |
| } |
| |
| return version; |
| } |
| |
| /** |
| * Returns the file extension. For example "test.png" returns "png". |
| * <p> |
| * If file has no extension, will return "bin" |
| * |
| * @param filename The file name to check |
| * @return The extension. If no extension is found, returns "bin". |
| */ |
| protected static String getFileExtension( final String filename ) { |
| String fileExt = "bin"; |
| |
| final int dot = filename.lastIndexOf('.'); |
| if( dot >= 0 && dot < filename.length()-1 ) { |
| fileExt = mangleName( filename.substring( dot+1 ) ); |
| } |
| |
| return fileExt; |
| } |
| |
| /** |
| * Writes the page properties back to the file system. |
| * Note that it WILL overwrite any previous properties. |
| */ |
| private void putPageProperties( final Attachment att, final Properties properties ) throws IOException, ProviderException { |
| final File attDir = findAttachmentDir( att ); |
| final File propertyFile = new File( attDir, PROPERTY_FILE ); |
| try( final OutputStream out = new FileOutputStream( propertyFile ) ) { |
| properties.store( out, " JSPWiki page properties for " + att.getName() + ". DO NOT MODIFY!" ); |
| } |
| } |
| |
| /** |
| * Reads page properties from the file system. |
| */ |
| private Properties getPageProperties( final Attachment att ) throws IOException, ProviderException { |
| final Properties props = new Properties(); |
| final File propertyFile = new File( findAttachmentDir(att), PROPERTY_FILE ); |
| if( propertyFile.exists() ) { |
| try( final InputStream in = new FileInputStream( propertyFile ) ) { |
| props.load( in ); |
| } |
| } |
| |
| return props; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void putAttachmentData( final Attachment att, final InputStream data ) throws ProviderException, IOException { |
| final File attDir = findAttachmentDir( att ); |
| |
| if( !attDir.exists() ) { |
| attDir.mkdirs(); |
| } |
| final int latestVersion = findLatestVersion( att ); |
| final int versionNumber = latestVersion + 1; |
| |
| final File newfile = new File( attDir, versionNumber + "." + getFileExtension( att.getFileName() ) ); |
| try( final OutputStream out = new FileOutputStream( newfile ) ) { |
| log.info( "Uploading attachment " + att.getFileName() + " to page " + att.getParentName() ); |
| log.info( "Saving attachment contents to " + newfile.getAbsolutePath() ); |
| FileUtil.copyContents( data, out ); |
| |
| final Properties props = getPageProperties( att ); |
| |
| String author = att.getAuthor(); |
| if( author == null ) { |
| author = "unknown"; // FIXME: Should be localized, but cannot due to missing WikiContext |
| } |
| props.setProperty( versionNumber + ".author", author ); |
| |
| final String changeNote = att.getAttribute( Page.CHANGENOTE ); |
| if( changeNote != null ) { |
| props.setProperty( versionNumber + ".changenote", changeNote ); |
| } |
| |
| putPageProperties( att, props ); |
| } catch( final IOException e ) { |
| log.error( "Could not save attachment data: ", e ); |
| throw (IOException) e.fillInStackTrace(); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String getProviderInfo() { |
| return ""; |
| } |
| |
| private File findFile( final File dir, final Attachment att ) throws FileNotFoundException, ProviderException { |
| int version = att.getVersion(); |
| if( version == WikiProvider.LATEST_VERSION ) { |
| version = findLatestVersion( att ); |
| } |
| |
| final String ext = getFileExtension( att.getFileName() ); |
| File f = new File( dir, version + "." + ext ); |
| |
| if( !f.exists() ) { |
| if( "bin".equals( ext ) ) { |
| final File fOld = new File( dir, version + "." ); |
| if( fOld.exists() ) { |
| f = fOld; |
| } |
| } |
| if( !f.exists() ) { |
| throw new FileNotFoundException( "No such file: " + f.getAbsolutePath() + " exists." ); |
| } |
| } |
| |
| return f; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public InputStream getAttachmentData( final Attachment att ) throws IOException, ProviderException { |
| final File attDir = findAttachmentDir( att ); |
| try { |
| final File f = findFile( attDir, att ); |
| return new FileInputStream( f ); |
| } catch( final FileNotFoundException e ) { |
| log.error( "File not found: " + e.getMessage() ); |
| throw new ProviderException( "No such page was found." ); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public List< Attachment > listAttachments( final Page page ) throws ProviderException { |
| final List< Attachment > result = new ArrayList<>(); |
| final File dir = findPageDir( page.getName() ); |
| final String[] attachments = dir.list(); |
| if( attachments != null ) { |
| // We now have a list of all potential attachments in the directory. |
| for( final String attachment : attachments ) { |
| final File f = new File( dir, attachment ); |
| if( f.isDirectory() ) { |
| String attachmentName = unmangleName( attachment ); |
| |
| // Is it a new-stylea attachment directory? If yes, we'll just deduce the name. If not, however, |
| // we'll check if there's a suitable property file in the directory. |
| if( attachmentName.endsWith( ATTDIR_EXTENSION ) ) { |
| attachmentName = attachmentName.substring( 0, attachmentName.length() - ATTDIR_EXTENSION.length() ); |
| } else { |
| final File propFile = new File( f, PROPERTY_FILE ); |
| if( !propFile.exists() ) { |
| // This is not obviously a JSPWiki attachment, so let's just skip it. |
| continue; |
| } |
| } |
| |
| final Attachment att = getAttachmentInfo( page, attachmentName, WikiProvider.LATEST_VERSION ); |
| // Sanity check - shouldn't really be happening, unless you mess with the repository directly. |
| if( att == null ) { |
| throw new ProviderException( "Attachment disappeared while reading information:" |
| + " if you did not touch the repository, there is a serious bug somewhere. " + "Attachment = " + attachment |
| + ", decoded = " + attachmentName ); |
| } |
| |
| result.add( att ); |
| } |
| } |
| } |
| |
| return result; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public Collection< Attachment > findAttachments( final QueryItem[] query ) { |
| return new ArrayList<>(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| // FIXME: Very unoptimized. |
| @Override |
| public List< Attachment > listAllChanged( final Date timestamp ) throws ProviderException { |
| final File attDir = new File( m_storageDir ); |
| if( !attDir.exists() ) { |
| throw new ProviderException( "Specified attachment directory " + m_storageDir + " does not exist!" ); |
| } |
| |
| final ArrayList< Attachment > list = new ArrayList<>(); |
| final String[] pagesWithAttachments = attDir.list( new AttachmentFilter() ); |
| |
| if( pagesWithAttachments != null ) { |
| for( final String pagesWithAttachment : pagesWithAttachments ) { |
| String pageId = unmangleName( pagesWithAttachment ); |
| pageId = pageId.substring( 0, pageId.length() - DIR_EXTENSION.length() ); |
| |
| final Collection< Attachment > c = listAttachments( Wiki.contents().page( m_engine, pageId ) ); |
| for( final Attachment att : c ) { |
| if( att.getLastModified().after( timestamp ) ) { |
| list.add( att ); |
| } |
| } |
| } |
| } |
| |
| list.sort( new PageTimeComparator() ); |
| |
| return list; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public Attachment getAttachmentInfo( final Page page, final String name, int version ) throws ProviderException { |
| final Attachment att = new org.apache.wiki.attachment.Attachment( m_engine, page.getName(), name ); |
| final File dir = findAttachmentDir( att ); |
| if( !dir.exists() ) { |
| // log.debug("Attachment dir not found - thus no attachment can exist."); |
| return null; |
| } |
| |
| if( version == WikiProvider.LATEST_VERSION ) { |
| version = findLatestVersion(att); |
| } |
| |
| att.setVersion( version ); |
| |
| // Should attachment be cachable by the client (browser)? |
| if( m_disableCache != null ) { |
| final Matcher matcher = m_disableCache.matcher( name ); |
| if( matcher.matches() ) { |
| att.setCacheable( false ); |
| } |
| } |
| |
| // System.out.println("Fetching info on version "+version); |
| try { |
| final Properties props = getPageProperties( att ); |
| att.setAuthor( props.getProperty( version+".author" ) ); |
| final String changeNote = props.getProperty( version+".changenote" ); |
| if( changeNote != null ) { |
| att.setAttribute( Page.CHANGENOTE, changeNote ); |
| } |
| |
| final File f = findFile( dir, att ); |
| att.setSize( f.length() ); |
| att.setLastModified( new Date( f.lastModified() ) ); |
| } catch( final FileNotFoundException e ) { |
| log.error( "Can't get attachment properties for " + att, e ); |
| return null; |
| } catch( final IOException e ) { |
| log.error("Can't read page properties", e ); |
| throw new ProviderException("Cannot read page properties: "+e.getMessage()); |
| } |
| // FIXME: Check for existence of this particular version. |
| |
| return att; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public List< Attachment > getVersionHistory( final Attachment att ) { |
| final ArrayList< Attachment > list = new ArrayList<>(); |
| try { |
| final int latest = findLatestVersion( att ); |
| for( int i = latest; i >= 1; i-- ) { |
| final Attachment a = getAttachmentInfo( Wiki.contents().page( m_engine, att.getParentName() ), att.getFileName(), i ); |
| if( a != null ) { |
| list.add( a ); |
| } |
| } |
| } catch( final ProviderException e ) { |
| log.error( "Getting version history failed for page: " + att, e ); |
| // FIXME: Should this fail? |
| } |
| |
| return list; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void deleteVersion( final Attachment att ) throws ProviderException { |
| // FIXME: Does nothing yet. |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void deleteAttachment( final Attachment att ) throws ProviderException { |
| final File dir = findAttachmentDir( att ); |
| final String[] files = dir.list(); |
| for( final String s : files ) { |
| final File file = new File( dir.getAbsolutePath() + "/" + s ); |
| file.delete(); |
| } |
| dir.delete(); |
| } |
| |
| /** |
| * Returns only those directories that contain attachments. |
| */ |
| public static class AttachmentFilter implements FilenameFilter { |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean accept( final File dir, final String name ) |
| { |
| return name.endsWith( DIR_EXTENSION ); |
| } |
| } |
| |
| /** |
| * Accepts only files that are actual versions, no control files. |
| */ |
| public static class AttachmentVersionFilter implements FilenameFilter { |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean accept( final File dir, final String name ) |
| { |
| return !name.equals( PROPERTY_FILE ); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void moveAttachmentsForPage( final String oldParent, final String newParent ) throws ProviderException { |
| final File srcDir = findPageDir( oldParent ); |
| final File destDir = findPageDir( newParent ); |
| |
| log.debug( "Trying to move all attachments from " + srcDir + " to " + destDir ); |
| |
| // If it exists, we're overwriting an old page (this has already been confirmed at a higher level), so delete any existing attachments. |
| if( destDir.exists() ) { |
| log.error( "Page rename failed because target directory " + destDir + " exists" ); |
| } else { |
| // destDir.getParentFile().mkdir(); |
| srcDir.renameTo( destDir ); |
| } |
| } |
| |
| } |
| |