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