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 org.apache.log4j.Logger; 022import org.apache.wiki.WikiEngine; 023import org.apache.wiki.WikiPage; 024import org.apache.wiki.WikiProvider; 025import org.apache.wiki.api.exceptions.NoRequiredPropertyException; 026import org.apache.wiki.api.exceptions.ProviderException; 027import org.apache.wiki.attachment.Attachment; 028import org.apache.wiki.pages.PageTimeComparator; 029import org.apache.wiki.search.QueryItem; 030import org.apache.wiki.util.FileUtil; 031import org.apache.wiki.util.TextUtil; 032 033import java.io.File; 034import java.io.FileInputStream; 035import java.io.FileNotFoundException; 036import java.io.FileOutputStream; 037import java.io.FilenameFilter; 038import java.io.IOException; 039import java.io.InputStream; 040import java.io.OutputStream; 041import java.util.ArrayList; 042import java.util.Collection; 043import java.util.Collections; 044import java.util.Date; 045import java.util.Iterator; 046import java.util.List; 047import java.util.Properties; 048import java.util.regex.Matcher; 049import java.util.regex.Pattern; 050 051/** 052 * Provides basic, versioning attachments. 053 * 054 * <PRE> 055 * Structure is as follows: 056 * attachment_dir/ 057 * ThisPage/ 058 * attachment.doc/ 059 * attachment.properties 060 * 1.doc 061 * 2.doc 062 * 3.doc 063 * picture.png/ 064 * attachment.properties 065 * 1.png 066 * 2.png 067 * ThatPage/ 068 * picture.png/ 069 * attachment.properties 070 * 1.png 071 * 072 * </PRE> 073 * 074 * The names of the directories will be URLencoded. 075 * <p> 076 * "attachment.properties" consists of the following items: 077 * <UL> 078 * <LI>1.author = author name for version 1 (etc) 079 * </UL> 080 */ 081public class BasicAttachmentProvider 082 implements WikiAttachmentProvider 083{ 084 private WikiEngine m_engine; 085 private String m_storageDir; 086 087 /** The property name for where the attachments should be stored. Value is <tt>{@value}</tt>. */ 088 public static final String PROP_STORAGEDIR = "jspwiki.basicAttachmentProvider.storageDir"; 089 090 /* 091 * Disable client cache for files with patterns 092 * since 2.5.96 093 */ 094 private Pattern m_disableCache = null; 095 096 /** The property name for specifying which attachments are not cached. Value is <tt>{@value}</tt>. */ 097 public static final String PROP_DISABLECACHE = "jspwiki.basicAttachmentProvider.disableCache"; 098 099 /** The name of the property file. */ 100 public static final String PROPERTY_FILE = "attachment.properties"; 101 102 /** The default extension for the page attachment directory name. */ 103 public static final String DIR_EXTENSION = "-att"; 104 105 /** The default extension for the attachment directory. */ 106 public static final String ATTDIR_EXTENSION = "-dir"; 107 108 static final Logger log = Logger.getLogger( BasicAttachmentProvider.class ); 109 110 /** 111 * {@inheritDoc} 112 */ 113 @Override 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( final Attachment att, final Properties properties ) throws IOException, ProviderException { 291 final File attDir = findAttachmentDir( att ); 292 final File propertyFile = new File( attDir, PROPERTY_FILE ); 293 try( OutputStream out = new FileOutputStream( propertyFile ) ) { 294 properties.store( out, " JSPWiki page properties for " + att.getName() + ". DO NOT MODIFY!" ); 295 } 296 } 297 298 /** 299 * Reads page properties from the file system. 300 */ 301 private Properties getPageProperties( final Attachment att ) throws IOException, ProviderException { 302 final Properties props = new Properties(); 303 final File propertyFile = new File( findAttachmentDir(att), PROPERTY_FILE ); 304 if( propertyFile.exists() ) { 305 try( final InputStream in = new FileInputStream( propertyFile ) ) { 306 props.load( in ); 307 } 308 } 309 310 return props; 311 } 312 313 /** 314 * {@inheritDoc} 315 */ 316 @Override 317 public void putAttachmentData( Attachment att, InputStream data ) throws ProviderException, IOException { 318 File attDir = findAttachmentDir( att ); 319 320 if(!attDir.exists()) 321 { 322 attDir.mkdirs(); 323 } 324 325 int latestVersion = findLatestVersion( att ); 326 327 // System.out.println("Latest version is "+latestVersion); 328 int versionNumber = latestVersion+1; 329 330 File newfile = new File( attDir, versionNumber + "." + getFileExtension( att.getFileName() ) ); 331 try( final OutputStream out = new FileOutputStream( newfile ) ) { 332 log.info("Uploading attachment "+att.getFileName()+" to page "+att.getParentName()); 333 log.info("Saving attachment contents to "+newfile.getAbsolutePath()); 334 335 FileUtil.copyContents( data, out ); 336 337 Properties props = getPageProperties( att ); 338 339 String author = att.getAuthor(); 340 341 if( author == null ) 342 { 343 author = "unknown"; // FIXME: Should be localized, but cannot due to missing WikiContext 344 } 345 346 props.setProperty( versionNumber+".author", author ); 347 348 String changeNote = (String)att.getAttribute(WikiPage.CHANGENOTE); 349 if( changeNote != null ) 350 { 351 props.setProperty( versionNumber+".changenote", changeNote ); 352 } 353 354 putPageProperties( att, props ); 355 } catch( IOException e ) { 356 log.error( "Could not save attachment data: ", e ); 357 throw (IOException) e.fillInStackTrace(); 358 } 359 } 360 361 /** 362 * {@inheritDoc} 363 */ 364 @Override 365 public String getProviderInfo() { 366 return ""; 367 } 368 369 private File findFile( File dir, Attachment att ) 370 throws FileNotFoundException, 371 ProviderException 372 { 373 int version = att.getVersion(); 374 375 if( version == WikiProvider.LATEST_VERSION ) 376 { 377 version = findLatestVersion( att ); 378 } 379 380 String ext = getFileExtension( att.getFileName() ); 381 File f = new File( dir, version+"."+ext ); 382 383 if( !f.exists() ) 384 { 385 if ("bin".equals(ext)) 386 { 387 File fOld = new File( dir, version+"." ); 388 if (fOld.exists()) 389 f = fOld; 390 } 391 if( !f.exists() ) 392 { 393 throw new FileNotFoundException("No such file: "+f.getAbsolutePath()+" exists."); 394 } 395 } 396 397 return f; 398 } 399 400 /** 401 * {@inheritDoc} 402 */ 403 @Override 404 public InputStream getAttachmentData( Attachment att ) 405 throws IOException, 406 ProviderException 407 { 408 File attDir = findAttachmentDir( att ); 409 410 try 411 { 412 File f = findFile( attDir, att ); 413 414 return new FileInputStream( f ); 415 } 416 catch( FileNotFoundException e ) 417 { 418 log.error("File not found: "+e.getMessage()); 419 throw new ProviderException("No such page was found."); 420 } 421 } 422 423 /** 424 * {@inheritDoc} 425 */ 426 @Override 427 public List< Attachment > listAttachments( WikiPage page ) 428 throws ProviderException 429 { 430 List<Attachment> result = new ArrayList<>(); 431 432 File dir = findPageDir( page.getName() ); 433 434 if( dir != null ) 435 { 436 String[] attachments = dir.list(); 437 438 if( attachments != null ) 439 { 440 // 441 // We now have a list of all potential attachments in 442 // the directory. 443 // 444 for( int i = 0; i < attachments.length; i++ ) 445 { 446 File f = new File( dir, attachments[i] ); 447 448 if( f.isDirectory() ) 449 { 450 String attachmentName = unmangleName( attachments[i] ); 451 452 // 453 // Is it a new-stylea attachment directory? If yes, 454 // we'll just deduce the name. If not, however, 455 // we'll check if there's a suitable property file 456 // in the directory. 457 // 458 if( attachmentName.endsWith( ATTDIR_EXTENSION ) ) 459 { 460 attachmentName = attachmentName.substring( 0, attachmentName.length()-ATTDIR_EXTENSION.length() ); 461 } 462 else 463 { 464 File propFile = new File( f, PROPERTY_FILE ); 465 466 if( !propFile.exists() ) 467 { 468 // 469 // This is not obviously a JSPWiki attachment, 470 // so let's just skip it. 471 // 472 continue; 473 } 474 } 475 476 Attachment att = getAttachmentInfo( page, attachmentName, 477 WikiProvider.LATEST_VERSION ); 478 479 // 480 // Sanity check - shouldn't really be happening, unless 481 // you mess with the repository directly. 482 // 483 if( att == null ) 484 { 485 throw new ProviderException("Attachment disappeared while reading information:"+ 486 " if you did not touch the repository, there is a serious bug somewhere. "+ 487 "Attachment = "+attachments[i]+ 488 ", decoded = "+attachmentName ); 489 } 490 491 result.add( att ); 492 } 493 } 494 } 495 } 496 497 return result; 498 } 499 500 /** 501 * {@inheritDoc} 502 */ 503 @Override 504 public Collection< Attachment > findAttachments( QueryItem[] query ) 505 { 506 return new ArrayList<>(); 507 } 508 509 /** 510 * {@inheritDoc} 511 */ 512 // FIXME: Very unoptimized. 513 @Override 514 public List<Attachment> listAllChanged( Date timestamp ) 515 throws ProviderException 516 { 517 File attDir = new File( m_storageDir ); 518 519 if( !attDir.exists() ) 520 { 521 throw new ProviderException("Specified attachment directory "+m_storageDir+" does not exist!"); 522 } 523 524 ArrayList<Attachment> list = new ArrayList<>(); 525 526 String[] pagesWithAttachments = attDir.list( new AttachmentFilter() ); 527 528 for( int i = 0; i < pagesWithAttachments.length; i++ ) 529 { 530 String pageId = unmangleName( pagesWithAttachments[i] ); 531 pageId = pageId.substring( 0, pageId.length()-DIR_EXTENSION.length() ); 532 533 Collection<Attachment> c = listAttachments( new WikiPage( m_engine, pageId ) ); 534 535 for( Iterator<Attachment> it = c.iterator(); it.hasNext(); ) 536 { 537 Attachment att = it.next(); 538 539 if( att.getLastModified().after( timestamp ) ) 540 { 541 list.add( att ); 542 } 543 } 544 } 545 546 Collections.sort( list, new PageTimeComparator() ); 547 548 return list; 549 } 550 551 /** 552 * {@inheritDoc} 553 */ 554 @Override 555 public Attachment getAttachmentInfo( final WikiPage page, final String name, int version ) throws ProviderException { 556 final Attachment att = new Attachment( m_engine, page.getName(), name ); 557 final File dir = findAttachmentDir( att ); 558 559 if( !dir.exists() ) { 560 // log.debug("Attachment dir not found - thus no attachment can exist."); 561 return null; 562 } 563 564 if( version == WikiProvider.LATEST_VERSION ) { 565 version = findLatestVersion(att); 566 } 567 568 att.setVersion( version ); 569 570 // Should attachment be cachable by the client (browser)? 571 if (m_disableCache != null) { 572 Matcher matcher = m_disableCache.matcher(name); 573 if (matcher.matches()) { 574 att.setCacheable(false); 575 } 576 } 577 578 // System.out.println("Fetching info on version "+version); 579 try { 580 Properties props = getPageProperties(att); 581 att.setAuthor( props.getProperty( version+".author" ) ); 582 final String changeNote = props.getProperty( version+".changenote" ); 583 if( changeNote != null ) { 584 att.setAttribute(WikiPage.CHANGENOTE, changeNote); 585 } 586 587 File f = findFile( dir, att ); 588 589 att.setSize( f.length() ); 590 att.setLastModified( new Date(f.lastModified()) ); 591 } catch( FileNotFoundException e ) { 592 log.error( "Can't get attachment properties for " + att, e ); 593 return null; 594 } catch( IOException e ) { 595 log.error("Can't read page properties", e ); 596 throw new ProviderException("Cannot read page properties: "+e.getMessage()); 597 } 598 // FIXME: Check for existence of this particular version. 599 600 return att; 601 } 602 603 /** 604 * {@inheritDoc} 605 */ 606 @Override 607 public List<Attachment> getVersionHistory( Attachment att ) 608 { 609 ArrayList<Attachment> list = new ArrayList<>(); 610 611 try 612 { 613 int latest = findLatestVersion( att ); 614 615 for( int i = latest; i >= 1; i-- ) 616 { 617 Attachment a = getAttachmentInfo( new WikiPage( m_engine, att.getParentName() ), 618 att.getFileName(), i ); 619 620 if( a != null ) 621 { 622 list.add( a ); 623 } 624 } 625 } 626 catch( ProviderException e ) 627 { 628 log.error("Getting version history failed for page: "+att,e); 629 // FIXME: SHould this fail? 630 } 631 632 return list; 633 } 634 635 /** 636 * {@inheritDoc} 637 */ 638 @Override 639 public void deleteVersion( Attachment att ) 640 throws ProviderException 641 { 642 // FIXME: Does nothing yet. 643 } 644 645 /** 646 * {@inheritDoc} 647 */ 648 @Override 649 public void deleteAttachment( Attachment att ) 650 throws ProviderException 651 { 652 File dir = findAttachmentDir( att ); 653 String[] files = dir.list(); 654 655 for( int i = 0; i < files.length; i++ ) 656 { 657 File file = new File( dir.getAbsolutePath() + "/" + files[i] ); 658 file.delete(); 659 } 660 dir.delete(); 661 } 662 663 664 /** 665 * Returns only those directories that contain attachments. 666 */ 667 public static class AttachmentFilter 668 implements FilenameFilter 669 { 670 /** 671 * {@inheritDoc} 672 */ 673 @Override 674 public boolean accept( File dir, String name ) 675 { 676 return name.endsWith( DIR_EXTENSION ); 677 } 678 } 679 680 /** 681 * Accepts only files that are actual versions, no control files. 682 */ 683 public static class AttachmentVersionFilter 684 implements FilenameFilter 685 { 686 /** 687 * {@inheritDoc} 688 */ 689 @Override 690 public boolean accept( File dir, String name ) 691 { 692 return !name.equals( PROPERTY_FILE ); 693 } 694 } 695 696 /** 697 * {@inheritDoc} 698 */ 699 700 @Override 701 public void moveAttachmentsForPage( String oldParent, String newParent ) 702 throws ProviderException 703 { 704 File srcDir = findPageDir( oldParent ); 705 File destDir = findPageDir( newParent ); 706 707 log.debug("Trying to move all attachments from "+srcDir+" to "+destDir); 708 709 // If it exists, we're overwriting an old page (this has already been 710 // confirmed at a higher level), so delete any existing attachments. 711 if (destDir.exists()) 712 { 713 log.error("Page rename failed because target dirctory "+destDir+" exists"); 714 } 715 else 716 { 717 //destDir.getParentFile().mkdir(); 718 srcDir.renameTo(destDir); 719 } 720 } 721} 722