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