001 /*
002 Licensed to the Apache Software Foundation (ASF) under one
003 or more contributor license agreements. See the NOTICE file
004 distributed with this work for additional information
005 regarding copyright ownership. The ASF licenses this file
006 to you under the Apache License, Version 2.0 (the
007 "License"); you may not use this file except in compliance
008 with the License. You may obtain a copy of the License at
009
010 http://www.apache.org/licenses/LICENSE-2.0
011
012 Unless required by applicable law or agreed to in writing,
013 software distributed under the License is distributed on an
014 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 KIND, either express or implied. See the License for the
016 specific language governing permissions and limitations
017 under the License.
018 */
019 package org.apache.wiki.providers;
020
021 import java.io.File;
022 import java.io.FileInputStream;
023 import java.io.FileNotFoundException;
024 import java.io.FileOutputStream;
025 import java.io.FilenameFilter;
026 import java.io.IOException;
027 import java.io.InputStream;
028 import java.io.OutputStream;
029 import java.util.ArrayList;
030 import java.util.Collection;
031 import java.util.Collections;
032 import java.util.Date;
033 import java.util.Iterator;
034 import java.util.List;
035 import java.util.Properties;
036 import java.util.regex.Matcher;
037 import java.util.regex.Pattern;
038
039 import org.apache.commons.io.IOUtils;
040 import org.apache.log4j.Logger;
041 import org.apache.wiki.WikiEngine;
042 import org.apache.wiki.WikiPage;
043 import org.apache.wiki.WikiProvider;
044 import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
045 import org.apache.wiki.api.exceptions.ProviderException;
046 import org.apache.wiki.attachment.Attachment;
047 import org.apache.wiki.search.QueryItem;
048 import org.apache.wiki.util.FileUtil;
049 import org.apache.wiki.util.TextUtil;
050 import org.apache.wiki.util.comparators.PageTimeComparator;
051
052 /**
053 * Provides basic, versioning attachments.
054 *
055 * <PRE>
056 * Structure is as follows:
057 * attachment_dir/
058 * ThisPage/
059 * attachment.doc/
060 * attachment.properties
061 * 1.doc
062 * 2.doc
063 * 3.doc
064 * picture.png/
065 * attachment.properties
066 * 1.png
067 * 2.png
068 * ThatPage/
069 * picture.png/
070 * attachment.properties
071 * 1.png
072 *
073 * </PRE>
074 *
075 * The names of the directories will be URLencoded.
076 * <p>
077 * "attachment.properties" consists of the following items:
078 * <UL>
079 * <LI>1.author = author name for version 1 (etc)
080 * </UL>
081 */
082 public class BasicAttachmentProvider
083 implements WikiAttachmentProvider
084 {
085 private WikiEngine m_engine;
086 private String m_storageDir;
087
088 /** The property name for where the attachments should be stored. Value is <tt>{@value}</tt>. */
089 public static final String PROP_STORAGEDIR = "jspwiki.basicAttachmentProvider.storageDir";
090
091 /*
092 * Disable client cache for files with patterns
093 * since 2.5.96
094 */
095 private Pattern m_disableCache = null;
096
097 /** The property name for specifying which attachments are not cached. Value is <tt>{@value}</tt>. */
098 public static final String PROP_DISABLECACHE = "jspwiki.basicAttachmentProvider.disableCache";
099
100 /** The name of the property file. */
101 public static final String PROPERTY_FILE = "attachment.properties";
102
103 /** The default extension for the page attachment directory name. */
104 public static final String DIR_EXTENSION = "-att";
105
106 /** The default extension for the attachment directory. */
107 public static final String ATTDIR_EXTENSION = "-dir";
108
109 static final Logger log = Logger.getLogger( BasicAttachmentProvider.class );
110
111 /**
112 * {@inheritDoc}
113 */
114 public void initialize( WikiEngine engine, Properties properties )
115 throws NoRequiredPropertyException,
116 IOException
117 {
118 m_engine = engine;
119 m_storageDir = TextUtil.getCanonicalFilePathProperty(properties, PROP_STORAGEDIR,
120 System.getProperty("user.home") + File.separator + "jspwiki-files");
121
122 String patternString = engine.getWikiProperties().getProperty( PROP_DISABLECACHE );
123 if ( patternString != null )
124 {
125 m_disableCache = Pattern.compile(patternString);
126 }
127
128 //
129 // Check if the directory exists - if it doesn't, create it.
130 //
131 File f = new File( m_storageDir );
132
133 if( !f.exists() )
134 {
135 f.mkdirs();
136 }
137
138 //
139 // Some sanity checks
140 //
141 if( !f.exists() )
142 throw new IOException("Could not find or create attachment storage directory '"+m_storageDir+"'");
143
144 if( !f.canWrite() )
145 throw new IOException("Cannot write to the attachment storage directory '"+m_storageDir+"'");
146
147 if( !f.isDirectory() )
148 throw new IOException("Your attachment storage points to a file, not a directory: '"+m_storageDir+"'");
149 }
150
151 /**
152 * Finds storage dir, and if it exists, makes sure that it is valid.
153 *
154 * @param wikipage Page to which this attachment is attached.
155 */
156 private File findPageDir( String wikipage )
157 throws ProviderException
158 {
159 wikipage = mangleName( wikipage );
160
161 File f = new File( m_storageDir, wikipage+DIR_EXTENSION );
162
163 if( f.exists() && !f.isDirectory() )
164 {
165 throw new ProviderException("Storage dir '"+f.getAbsolutePath()+"' is not a directory!");
166 }
167
168 return f;
169 }
170
171 private static String mangleName( String wikiname )
172 {
173 String res = TextUtil.urlEncodeUTF8( wikiname );
174
175 return res;
176 }
177
178 private static String unmangleName( String filename )
179 {
180 return TextUtil.urlDecodeUTF8( filename );
181 }
182
183 /**
184 * Finds the dir in which the attachment lives.
185 */
186 private File findAttachmentDir( Attachment att )
187 throws ProviderException
188 {
189 File f = new File( findPageDir(att.getParentName()),
190 mangleName(att.getFileName()+ATTDIR_EXTENSION) );
191
192 //
193 // Migration code for earlier versions of JSPWiki.
194 // Originally, we used plain filename. Then we realized we need
195 // to urlencode it. Then we realized that we have to use a
196 // postfix to make sure illegal file names are never formed.
197 //
198 if( !f.exists() )
199 {
200 File oldf = new File( findPageDir( att.getParentName() ),
201 mangleName( att.getFileName() ) );
202 if( oldf.exists() )
203 {
204 f = oldf;
205 }
206 else
207 {
208 oldf = new File( findPageDir( att.getParentName() ),
209 att.getFileName() );
210
211 if( oldf.exists() )
212 {
213 f = oldf;
214 }
215 }
216 }
217
218 return f;
219 }
220
221 /**
222 * Goes through the repository and decides which version is
223 * the newest one in that directory.
224 *
225 * @return Latest version number in the repository, or 0, if
226 * there is no page in the repository.
227 */
228 private int findLatestVersion( Attachment att )
229 throws ProviderException
230 {
231 // File pageDir = findPageDir( att.getName() );
232 File attDir = findAttachmentDir( att );
233
234 // log.debug("Finding pages in "+attDir.getAbsolutePath());
235 String[] pages = attDir.list( new AttachmentVersionFilter() );
236
237 if( pages == null )
238 {
239 return 0; // No such thing found.
240 }
241
242 int version = 0;
243
244 for( int i = 0; i < pages.length; i++ )
245 {
246 // log.debug("Checking: "+pages[i]);
247 int cutpoint = pages[i].indexOf( '.' );
248 String pageNum = ( cutpoint > 0 ) ? pages[i].substring( 0, cutpoint ) : pages[i] ;
249
250 try
251 {
252 int res = Integer.parseInt( pageNum );
253
254 if( res > version )
255 {
256 version = res;
257 }
258 }
259 catch( NumberFormatException e ) {} // It's okay to skip these.
260 }
261
262 return version;
263 }
264
265 /**
266 * Returns the file extension. For example "test.png" returns "png".
267 * <p>
268 * If file has no extension, will return "bin"
269 *
270 * @param filename The file name to check
271 * @return The extension. If no extension is found, returns "bin".
272 */
273 protected static String getFileExtension( String filename )
274 {
275 String fileExt = "bin";
276
277 int dot = filename.lastIndexOf('.');
278 if( dot >= 0 && dot < filename.length()-1 )
279 {
280 fileExt = mangleName( filename.substring( dot+1 ) );
281 }
282
283 return fileExt;
284 }
285
286 /**
287 * Writes the page properties back to the file system.
288 * Note that it WILL overwrite any previous properties.
289 */
290 private void putPageProperties( Attachment att, Properties properties ) throws IOException, ProviderException {
291 File attDir = findAttachmentDir( att );
292 File propertyFile = new File( attDir, PROPERTY_FILE );
293
294 OutputStream out = null;
295
296 try {
297 out = new FileOutputStream( propertyFile );
298 properties.store( out, " JSPWiki page properties for " + att.getName() + ". DO NOT MODIFY!" );
299 } catch ( IOException ioe ) {
300 IOUtils.closeQuietly( out );
301 throw ioe;
302 } finally {
303 IOUtils.closeQuietly( out );
304 }
305 }
306
307 /**
308 * Reads page properties from the file system.
309 */
310 private Properties getPageProperties( Attachment att ) throws IOException, ProviderException {
311 Properties props = new Properties();
312
313 File propertyFile = new File( findAttachmentDir(att), PROPERTY_FILE );
314
315 if( propertyFile.exists() ) {
316 InputStream in = null;
317 try {
318 in = new FileInputStream( propertyFile );
319 props.load( in );
320 } catch ( IOException ioe ) {
321 IOUtils.closeQuietly( in );
322 throw ioe;
323 } finally {
324 IOUtils.closeQuietly( in );
325 }
326 }
327
328 return props;
329 }
330
331 /**
332 * {@inheritDoc}
333 */
334 public void putAttachmentData( Attachment att, InputStream data ) throws ProviderException, IOException {
335 OutputStream out = null;
336 File attDir = findAttachmentDir( att );
337
338 if(!attDir.exists())
339 {
340 attDir.mkdirs();
341 }
342
343 int latestVersion = findLatestVersion( att );
344
345 // System.out.println("Latest version is "+latestVersion);
346
347 try
348 {
349 int versionNumber = latestVersion+1;
350
351 File newfile = new File( attDir, versionNumber+"."+
352 getFileExtension(att.getFileName()) );
353
354 log.info("Uploading attachment "+att.getFileName()+" to page "+att.getParentName());
355 log.info("Saving attachment contents to "+newfile.getAbsolutePath());
356 out = new FileOutputStream(newfile);
357
358 FileUtil.copyContents( data, out );
359
360 Properties props = getPageProperties( att );
361
362 String author = att.getAuthor();
363
364 if( author == null )
365 {
366 author = "unknown"; // FIXME: Should be localized, but cannot due to missing WikiContext
367 }
368
369 props.setProperty( versionNumber+".author", author );
370
371 String changeNote = (String)att.getAttribute(WikiPage.CHANGENOTE);
372 if( changeNote != null )
373 {
374 props.setProperty( versionNumber+".changenote", changeNote );
375 }
376
377 putPageProperties( att, props );
378 }
379 catch( IOException e )
380 {
381 log.error( "Could not save attachment data: ", e );
382 IOUtils.closeQuietly( out );
383 throw (IOException) e.fillInStackTrace();
384 }
385 finally
386 {
387 IOUtils.closeQuietly( out );
388 }
389 }
390
391 /**
392 * {@inheritDoc}
393 */
394 public String getProviderInfo()
395 {
396 return "";
397 }
398
399 private File findFile( File dir, Attachment att )
400 throws FileNotFoundException,
401 ProviderException
402 {
403 int version = att.getVersion();
404
405 if( version == WikiProvider.LATEST_VERSION )
406 {
407 version = findLatestVersion( att );
408 }
409
410 String ext = getFileExtension( att.getFileName() );
411 File f = new File( dir, version+"."+ext );
412
413 if( !f.exists() )
414 {
415 if ("bin".equals(ext))
416 {
417 File fOld = new File( dir, version+"." );
418 if (fOld.exists())
419 f = fOld;
420 }
421 if( !f.exists() )
422 {
423 throw new FileNotFoundException("No such file: "+f.getAbsolutePath()+" exists.");
424 }
425 }
426
427 return f;
428 }
429
430 /**
431 * {@inheritDoc}
432 */
433 public InputStream getAttachmentData( Attachment att )
434 throws IOException,
435 ProviderException
436 {
437 File attDir = findAttachmentDir( att );
438
439 try
440 {
441 File f = findFile( attDir, att );
442
443 return new FileInputStream( f );
444 }
445 catch( FileNotFoundException e )
446 {
447 log.error("File not found: "+e.getMessage());
448 throw new ProviderException("No such page was found.");
449 }
450 }
451
452 /**
453 * {@inheritDoc}
454 */
455 public Collection listAttachments( WikiPage page )
456 throws ProviderException
457 {
458 Collection<Attachment> result = new ArrayList<Attachment>();
459
460 File dir = findPageDir( page.getName() );
461
462 if( dir != null )
463 {
464 String[] attachments = dir.list();
465
466 if( attachments != null )
467 {
468 //
469 // We now have a list of all potential attachments in
470 // the directory.
471 //
472 for( int i = 0; i < attachments.length; i++ )
473 {
474 File f = new File( dir, attachments[i] );
475
476 if( f.isDirectory() )
477 {
478 String attachmentName = unmangleName( attachments[i] );
479
480 //
481 // Is it a new-stylea attachment directory? If yes,
482 // we'll just deduce the name. If not, however,
483 // we'll check if there's a suitable property file
484 // in the directory.
485 //
486 if( attachmentName.endsWith( ATTDIR_EXTENSION ) )
487 {
488 attachmentName = attachmentName.substring( 0, attachmentName.length()-ATTDIR_EXTENSION.length() );
489 }
490 else
491 {
492 File propFile = new File( f, PROPERTY_FILE );
493
494 if( !propFile.exists() )
495 {
496 //
497 // This is not obviously a JSPWiki attachment,
498 // so let's just skip it.
499 //
500 continue;
501 }
502 }
503
504 Attachment att = getAttachmentInfo( page, attachmentName,
505 WikiProvider.LATEST_VERSION );
506
507 //
508 // Sanity check - shouldn't really be happening, unless
509 // you mess with the repository directly.
510 //
511 if( att == null )
512 {
513 throw new ProviderException("Attachment disappeared while reading information:"+
514 " if you did not touch the repository, there is a serious bug somewhere. "+
515 "Attachment = "+attachments[i]+
516 ", decoded = "+attachmentName );
517 }
518
519 result.add( att );
520 }
521 }
522 }
523 }
524
525 return result;
526 }
527
528 /**
529 * {@inheritDoc}
530 */
531 public Collection findAttachments( QueryItem[] query )
532 {
533 return null;
534 }
535
536 /**
537 * {@inheritDoc}
538 */
539 // FIXME: Very unoptimized.
540 public List listAllChanged( Date timestamp )
541 throws ProviderException
542 {
543 File attDir = new File( m_storageDir );
544
545 if( !attDir.exists() )
546 {
547 throw new ProviderException("Specified attachment directory "+m_storageDir+" does not exist!");
548 }
549
550 ArrayList<Attachment> list = new ArrayList<Attachment>();
551
552 String[] pagesWithAttachments = attDir.list( new AttachmentFilter() );
553
554 for( int i = 0; i < pagesWithAttachments.length; i++ )
555 {
556 String pageId = unmangleName( pagesWithAttachments[i] );
557 pageId = pageId.substring( 0, pageId.length()-DIR_EXTENSION.length() );
558
559 Collection c = listAttachments( new WikiPage( m_engine, pageId ) );
560
561 for( Iterator it = c.iterator(); it.hasNext(); )
562 {
563 Attachment att = (Attachment) it.next();
564
565 if( att.getLastModified().after( timestamp ) )
566 {
567 list.add( att );
568 }
569 }
570 }
571
572 Collections.sort( list, new PageTimeComparator() );
573
574 return list;
575 }
576
577 /**
578 * {@inheritDoc}
579 */
580 public Attachment getAttachmentInfo( WikiPage page, String name, int version )
581 throws ProviderException
582 {
583 Attachment att = new Attachment( m_engine, page.getName(), name );
584 File dir = findAttachmentDir( att );
585
586 if( !dir.exists() )
587 {
588 // log.debug("Attachment dir not found - thus no attachment can exist.");
589 return null;
590 }
591
592 if( version == WikiProvider.LATEST_VERSION )
593 {
594 version = findLatestVersion(att);
595 }
596
597 att.setVersion( version );
598
599 // Should attachment be cachable by the client (browser)?
600 if (m_disableCache != null)
601 {
602 Matcher matcher = m_disableCache.matcher(name);
603 if (matcher.matches())
604 {
605 att.setCacheable(false);
606 }
607 }
608
609
610 // System.out.println("Fetching info on version "+version);
611 try
612 {
613 Properties props = getPageProperties(att);
614
615 att.setAuthor( props.getProperty( version+".author" ) );
616
617 String changeNote = props.getProperty( version+".changenote" );
618 if( changeNote != null )
619 {
620 att.setAttribute(WikiPage.CHANGENOTE, changeNote);
621 }
622
623 File f = findFile( dir, att );
624
625 att.setSize( f.length() );
626 att.setLastModified( new Date(f.lastModified()) );
627 }
628 catch( FileNotFoundException e )
629 {
630 log.error( "Can't get attachment properties for " + att, e );
631 return null;
632 }
633 catch( IOException e )
634 {
635 log.error("Can't read page properties", e );
636 throw new ProviderException("Cannot read page properties: "+e.getMessage());
637 }
638 // FIXME: Check for existence of this particular version.
639
640 return att;
641 }
642
643 /**
644 * {@inheritDoc}
645 */
646 public List getVersionHistory( Attachment att )
647 {
648 ArrayList<Attachment> list = new ArrayList<Attachment>();
649
650 try
651 {
652 int latest = findLatestVersion( att );
653
654 for( int i = latest; i >= 1; i-- )
655 {
656 Attachment a = getAttachmentInfo( new WikiPage( m_engine, att.getParentName() ),
657 att.getFileName(), i );
658
659 if( a != null )
660 {
661 list.add( a );
662 }
663 }
664 }
665 catch( ProviderException e )
666 {
667 log.error("Getting version history failed for page: "+att,e);
668 // FIXME: SHould this fail?
669 }
670
671 return list;
672 }
673
674 /**
675 * {@inheritDoc}
676 */
677 public void deleteVersion( Attachment att )
678 throws ProviderException
679 {
680 // FIXME: Does nothing yet.
681 }
682
683 /**
684 * {@inheritDoc}
685 */
686 public void deleteAttachment( Attachment att )
687 throws ProviderException
688 {
689 File dir = findAttachmentDir( att );
690 String[] files = dir.list();
691
692 for( int i = 0; i < files.length; i++ )
693 {
694 File file = new File( dir.getAbsolutePath() + "/" + files[i] );
695 file.delete();
696 }
697 dir.delete();
698 }
699
700
701 /**
702 * Returns only those directories that contain attachments.
703 */
704 public static class AttachmentFilter
705 implements FilenameFilter
706 {
707 /**
708 * {@inheritDoc}
709 */
710 public boolean accept( File dir, String name )
711 {
712 return name.endsWith( DIR_EXTENSION );
713 }
714 }
715
716 /**
717 * Accepts only files that are actual versions, no control files.
718 */
719 public static class AttachmentVersionFilter
720 implements FilenameFilter
721 {
722 /**
723 * {@inheritDoc}
724 */
725 public boolean accept( File dir, String name )
726 {
727 return !name.equals( PROPERTY_FILE );
728 }
729 }
730
731 /**
732 * {@inheritDoc}
733 */
734
735 public void moveAttachmentsForPage( String oldParent, String newParent )
736 throws ProviderException
737 {
738 File srcDir = findPageDir( oldParent );
739 File destDir = findPageDir( newParent );
740
741 log.debug("Trying to move all attachments from "+srcDir+" to "+destDir);
742
743 // If it exists, we're overwriting an old page (this has already been
744 // confirmed at a higher level), so delete any existing attachments.
745 if (destDir.exists())
746 {
747 log.error("Page rename failed because target dirctory "+destDir+" exists");
748 }
749 else
750 {
751 //destDir.getParentFile().mkdir();
752 srcDir.renameTo(destDir);
753 }
754 }
755 }
756