From 268a6b9603a77ed3f59e25cf0354c444a6b0d850 Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Tue, 28 May 2013 14:28:57 +0200
Subject: [PATCH] OO-617: move the BLOBs from emails attachments to the file
 system

---
 .../persistence/_spring/core_persistence.xml  |   4 +-
 .../olat/core/util/mail/MailAttachment.java   |  21 ++
 .../org/olat/core/util/mail/MailModule.java   |  18 ++
 .../core/util/mail/manager/MailManager.java   | 188 +++++++++++++++---
 .../util/mail/model/DBMailAttachment.hbm.xml  |  15 +-
 .../util/mail/model/DBMailAttachment.java     |  32 ++-
 .../util/mail/ui/MailAttachmentMapper.java    |  64 +-----
 .../util/vfs}/FileStorage.java                |  61 +++---
 .../ims/qti/qpool/QTIExportProcessor.java     |   6 +-
 .../ims/qti/qpool/QTIImportProcessor.java     |   8 +-
 .../qti/qpool/QTIQPoolServiceProvider.java    |   4 +-
 .../qpool/impl/FileQPoolServiceProvider.java  |   6 +-
 .../qpool/impl/TextQPoolServiceProvider.java  |   6 +-
 .../manager/AbstractQPoolServiceProvider.java |   2 +-
 .../qpool/manager/QPoolFileStorage.java       |  63 ++++++
 .../qpool/manager/QuestionItemDAO.java        |   2 +-
 .../org/olat/upgrade/OLATUpgrade_9_0_0.java   | 145 ++++++++++++++
 .../olat/upgrade/_spring/upgradeContext.xml   |   1 +
 .../model/DBMailAttachmentData.hbm.xml        |  22 ++
 .../model/DBMailAttachmentData.java           |  35 +++-
 .../database/mysql/alter_8_4_0_to_9_0_0.sql   |  10 +
 .../database/mysql/setupDatabase.sql          |  22 +-
 .../database/oracle/alter_8_4_0_to_9_0_0.sql  |   9 +
 .../database/oracle/setupDatabase.sql         |   6 +
 .../postgresql/alter_8_4_0_to_9_0_0.sql       |   8 +
 .../database/postgresql/setupDatabase.sql     |  22 +-
 .../ims/qti/qpool/QTIExportProcessorTest.java |   4 +-
 .../ims/qti/qpool/QTIImportProcessorTest.java |   4 +-
 .../qpool/manager/FileStorageTest.java        |   2 +-
 29 files changed, 626 insertions(+), 164 deletions(-)
 create mode 100644 src/main/java/org/olat/core/util/mail/MailAttachment.java
 rename src/main/java/org/olat/{modules/qpool/manager => core/util/vfs}/FileStorage.java (78%)
 create mode 100644 src/main/java/org/olat/modules/qpool/manager/QPoolFileStorage.java
 create mode 100644 src/main/java/org/olat/upgrade/OLATUpgrade_9_0_0.java
 create mode 100644 src/main/java/org/olat/upgrade/model/DBMailAttachmentData.hbm.xml
 rename src/main/java/org/olat/{core/util/mail => upgrade}/model/DBMailAttachmentData.java (76%)

diff --git a/src/main/java/org/olat/core/commons/persistence/_spring/core_persistence.xml b/src/main/java/org/olat/core/commons/persistence/_spring/core_persistence.xml
index 0b2db1bc748..775bf283801 100644
--- a/src/main/java/org/olat/core/commons/persistence/_spring/core_persistence.xml
+++ b/src/main/java/org/olat/core/commons/persistence/_spring/core_persistence.xml
@@ -55,7 +55,6 @@
 		<mapping-file>org/olat/modules/openmeetings/model/OpenMeetingsReference.hbm.xml</mapping-file>
 		<mapping-file>org/olat/properties/Property.hbm.xml</mapping-file>
 		<mapping-file>org/olat/catalog/CatalogEntryImpl.hbm.xml</mapping-file>
-		<mapping-file>org/olat/upgrade/model/BookmarkImpl.hbm.xml</mapping-file>
 		<mapping-file>org/olat/notifications/PublisherImpl.hbm.xml</mapping-file>
 		<mapping-file>org/olat/notifications/SubscriberImpl.hbm.xml</mapping-file>
 		<mapping-file>org/olat/registration/TemporaryKeyImpl.hbm.xml</mapping-file>
@@ -87,6 +86,9 @@
 		<mapping-file>org/olat/course/db/impl/CourseDBEntryImpl.hbm.xml</mapping-file>
 		<mapping-file>org/olat/modules/coach/model/EfficiencyStatementStatEntry.hbm.xml</mapping-file>
 		
+		<mapping-file>org/olat/upgrade/model/BookmarkImpl.hbm.xml</mapping-file>
+		<mapping-file>org/olat/upgrade/model/DBMailAttachmentData.hbm.xml</mapping-file>
+		
 		<class>org.olat.core.dispatcher.mapper.model.PersistedMapper</class>
 		<class>org.olat.group.model.BusinessGroupParticipantViewImpl</class>
 		<class>org.olat.group.model.BusinessGroupOwnerViewImpl</class>
diff --git a/src/main/java/org/olat/core/util/mail/MailAttachment.java b/src/main/java/org/olat/core/util/mail/MailAttachment.java
new file mode 100644
index 00000000000..fccdbd9a625
--- /dev/null
+++ b/src/main/java/org/olat/core/util/mail/MailAttachment.java
@@ -0,0 +1,21 @@
+package org.olat.core.util.mail;
+
+/**
+ * 
+ * Initial date: 28.05.2013<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public interface MailAttachment {
+	
+	public String getName();
+	
+	public String getPath();
+	
+	public Long getChecksum();
+	
+	public Long getSize();
+	
+	public String getMimetype();
+
+}
diff --git a/src/main/java/org/olat/core/util/mail/MailModule.java b/src/main/java/org/olat/core/util/mail/MailModule.java
index aa8b7926e9d..b105e143465 100644
--- a/src/main/java/org/olat/core/util/mail/MailModule.java
+++ b/src/main/java/org/olat/core/util/mail/MailModule.java
@@ -19,7 +19,10 @@
  */
 package org.olat.core.util.mail;
 
+import java.io.File;
+
 import org.olat.core.CoreSpringFactory;
+import org.olat.core.commons.modules.bc.FolderConfig;
 import org.olat.core.configuration.AbstractOLATModule;
 import org.olat.core.configuration.PersistedProperties;
 import org.olat.core.extensions.action.GenericActionExtension;
@@ -28,6 +31,8 @@ import org.olat.core.util.StringHelper;
 import org.olat.core.util.WebappHelper;
 import org.olat.core.util.event.FrameworkStartedEvent;
 import org.olat.core.util.event.FrameworkStartupEventChannel;
+import org.olat.core.util.vfs.LocalFolderImpl;
+import org.olat.core.util.vfs.VFSContainer;
 
 /**
  * 
@@ -47,6 +52,9 @@ public class MailModule extends AbstractOLATModule {
 	private boolean receiveRealMailUserDefaultSetting;
 	private int maxSizeOfAttachments = 5;
 	
+	private static final String ATTACHMENT_DEFAULT = "/mail";
+	private String attachmentsRoot = ATTACHMENT_DEFAULT;
+	
 	private WebappHelper webappHelper;
 	
 	public MailModule() {
@@ -164,6 +172,16 @@ public class MailModule extends AbstractOLATModule {
 		}
 		return maxSizeOfAttachments;
 	}
+	
+	public VFSContainer getRootForAttachments() {
+		String root = FolderConfig.getCanonicalRoot() + attachmentsRoot;
+		File rootFile = new File(root);
+		if(!rootFile.exists()) {
+			rootFile.mkdirs();
+		}
+		VFSContainer rootContainer = new LocalFolderImpl(rootFile);
+		return rootContainer;
+	}
 
 	/**
 	 * @return the configured mail host. Can be null, indicating that the system
diff --git a/src/main/java/org/olat/core/util/mail/manager/MailManager.java b/src/main/java/org/olat/core/util/mail/manager/MailManager.java
index b6c979edcd1..5b1fa75e500 100644
--- a/src/main/java/org/olat/core/util/mail/manager/MailManager.java
+++ b/src/main/java/org/olat/core/util/mail/manager/MailManager.java
@@ -25,12 +25,16 @@ import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Properties;
+import java.util.Set;
+import java.util.zip.Adler32;
 
 import javax.activation.DataHandler;
 import javax.activation.DataSource;
@@ -48,10 +52,10 @@ import javax.mail.internet.InternetAddress;
 import javax.mail.internet.MimeBodyPart;
 import javax.mail.internet.MimeMessage;
 import javax.mail.internet.MimeMultipart;
-import javax.mail.util.ByteArrayDataSource;
 import javax.persistence.TemporalType;
 import javax.persistence.TypedQuery;
 
+import org.apache.commons.io.FileUtils;
 import org.apache.commons.io.IOUtils;
 import org.olat.core.commons.persistence.DB;
 import org.olat.core.commons.persistence.PersistentObject;
@@ -60,16 +64,17 @@ import org.olat.core.id.Identity;
 import org.olat.core.id.OLATResourceable;
 import org.olat.core.id.UserConstants;
 import org.olat.core.manager.BasicManager;
+import org.olat.core.util.Encoder;
 import org.olat.core.util.StringHelper;
 import org.olat.core.util.WebappHelper;
 import org.olat.core.util.mail.ContactList;
+import org.olat.core.util.mail.MailAttachment;
 import org.olat.core.util.mail.MailContext;
 import org.olat.core.util.mail.MailModule;
 import org.olat.core.util.mail.MailerResult;
 import org.olat.core.util.mail.MailerSMTPAuthenticator;
 import org.olat.core.util.mail.model.DBMail;
 import org.olat.core.util.mail.model.DBMailAttachment;
-import org.olat.core.util.mail.model.DBMailAttachmentData;
 import org.olat.core.util.mail.model.DBMailImpl;
 import org.olat.core.util.mail.model.DBMailRecipient;
 import org.olat.core.util.notifications.NotificationsManager;
@@ -77,6 +82,11 @@ import org.olat.core.util.notifications.Publisher;
 import org.olat.core.util.notifications.PublisherData;
 import org.olat.core.util.notifications.Subscriber;
 import org.olat.core.util.notifications.SubscriptionContext;
+import org.olat.core.util.vfs.FileStorage;
+import org.olat.core.util.vfs.VFSContainer;
+import org.olat.core.util.vfs.VFSItem;
+import org.olat.core.util.vfs.VFSLeaf;
+import org.olat.core.util.vfs.VFSManager;
 
 /**
  * 
@@ -93,6 +103,7 @@ public class MailManager extends BasicManager {
 	
 	private final MailModule mailModule;
 	private DB dbInstance;
+	private FileStorage attachmentStorage;
 	private NotificationsManager notificationsManager;
 	
 	private static MailManager INSTANCE;
@@ -127,6 +138,9 @@ public class MailManager extends BasicManager {
 	 * [used by Spring]
 	 */
 	public void init() {
+		VFSContainer root = mailModule.getRootForAttachments();
+		attachmentStorage = new FileStorage(root);
+		
 		PublisherData pdata = getPublisherData();
 		SubscriptionContext scontext = getSubscriptionContext();
 		notificationsManager.getOrCreatePublisher(scontext, pdata);
@@ -187,17 +201,89 @@ public class MailManager extends BasicManager {
 				.getResultList();
 	}
 	
-	public DBMailAttachmentData getAttachmentWithData(Long key) {
+	public DBMailAttachment getAttachment(Long key) {
 		StringBuilder sb = new StringBuilder();
-		sb.append("select attachment from ").append(DBMailAttachmentData.class.getName()).append(" attachment")
+		sb.append("select attachment from ").append(DBMailAttachment.class.getName()).append(" attachment")
 			.append(" where attachment.key=:attachmentKey");
 
-		List<DBMailAttachmentData> mails = dbInstance.getCurrentEntityManager()
-				.createQuery(sb.toString(), DBMailAttachmentData.class)
+		List<DBMailAttachment> attachments = dbInstance.getCurrentEntityManager()
+				.createQuery(sb.toString(), DBMailAttachment.class)
 				.setParameter("attachmentKey", key)
 				.getResultList();
-		if(mails.isEmpty()) return null;
-		return mails.get(0);
+
+		if(attachments.isEmpty()) {
+			return null;
+		}
+		return attachments.get(0);
+	}
+	
+	public String saveAttachmentToStorage(String name, String mimetype, long checksum, long size, InputStream stream) {
+		String hasSibling = getAttachmentSibling(name, mimetype, checksum, size);
+		if(StringHelper.containsNonWhitespace(hasSibling)) {
+			return hasSibling;
+		}
+		
+		String uuid = Encoder.encrypt(name + checksum);
+		String dir = attachmentStorage.generateDir(uuid, false);
+		VFSContainer container = attachmentStorage.getContainer(dir);
+		String uniqueName = VFSManager.similarButNonExistingName(container, name);
+		VFSLeaf file = container.createChildLeaf(uniqueName);
+		VFSManager.copyContent(stream, file);
+		return dir + uniqueName;
+	}
+	
+	public String getAttachmentSibling(String name, String mimetype, long checksum, long size) {
+		StringBuilder sb = new StringBuilder();
+		sb.append("select attachment from ").append(DBMailAttachment.class.getName()).append(" attachment")
+			.append(" where attachment.checksum=:checksum and attachment.size=:size and attachment.name=:name");
+		if(mimetype == null) {
+			sb.append(" and attachment.mimetype is null");
+		} else {
+			sb.append(" and attachment.mimetype=:mimetype");
+		}
+		
+		TypedQuery<DBMailAttachment> query = dbInstance.getCurrentEntityManager()
+				.createQuery(sb.toString(), DBMailAttachment.class)
+				.setParameter("checksum", new Long(checksum))
+				.setParameter("size", new Long(size))
+				.setParameter("name", name);
+		if(mimetype != null) {
+			query.setParameter("mimetype", mimetype);
+		}
+
+		List<DBMailAttachment> attachments = query.getResultList();
+		if(attachments.isEmpty()) {
+			return null;
+		}
+		return attachments.get(0).getPath();
+	}
+	
+	public int countAttachment(String path) {
+		StringBuilder sb = new StringBuilder();
+		sb.append("select count(attachment) from ").append(DBMailAttachment.class.getName()).append(" attachment")
+			.append(" where attachment.path=:path");
+
+		return dbInstance.getCurrentEntityManager()
+				.createQuery(sb.toString(), Number.class)
+				.setParameter("path", path)
+				.getSingleResult().intValue();
+	}
+	
+	public VFSLeaf getAttachmentDatas(Long key) {
+		DBMailAttachment attachment = getAttachment(key);
+		return getAttachmentDatas(attachment);
+	}
+	
+	public VFSLeaf getAttachmentDatas(MailAttachment attachment) {
+		String path = attachment.getPath();
+		if(StringHelper.containsNonWhitespace(path)) {
+			VFSContainer root = mailModule.getRootForAttachments();
+			VFSItem item = root.resolve(path);
+			if(item instanceof VFSLeaf) {
+				return (VFSLeaf)item;
+			}
+		}
+		return null;
 	}
 	
 	public boolean hasNewMail(Identity identity) {
@@ -341,13 +427,29 @@ public class MailManager extends BasicManager {
 		}
 		
 		if(delete) {
+			Set<String> paths = new HashSet<String>();
+			
 			//all marked as deleted -> delete the mail
 			List<DBMailAttachment> attachments = getAttachments(mail);
 			for(DBMailAttachment attachment: attachments) {
 				mail = attachment.getMail();//reload from the hibernate session
 				dbInstance.deleteObject(attachment);
+				if(StringHelper.containsNonWhitespace(attachment.getPath())) {
+					paths.add(attachment.getPath());
+				}
 			}
 			dbInstance.deleteObject(mail);
+			
+			//try to remove orphans file
+			for(String path:paths) {
+				int count = countAttachment(path);
+				if(count == 0) {
+					VFSItem item = mailModule.getRootForAttachments().resolve(path);
+					if(item instanceof VFSLeaf) {
+						((VFSLeaf)item).delete();
+					}
+				}
+			}
 		} else {
 			for(DBMailRecipient update:updates) {
 				dbInstance.updateObject(update);
@@ -651,30 +753,34 @@ public class MailManager extends BasicManager {
 			//add bcc recipients
 			appendRecipients(mail, bccLists, toAddress, bccAddress, false, makeRealMail, result);
 			
-			dbInstance.saveObject(mail);
+			dbInstance.getCurrentEntityManager().persist(mail);
 			
 			//save attachments
 			if(attachments != null && !attachments.isEmpty()) {
 				for(File attachment:attachments) {
-					DBMailAttachmentData data = new DBMailAttachmentData();
-					data.setSize(attachment.length());
-					data.setName(attachment.getName());
-					data.setMimetype(WebappHelper.getMimeType(attachment.getName()));
-					data.setMail(mail);
-					
-					InputStream fis = null;
+
+					FileInputStream in = null;
 					try {
-						byte[] datas = new byte[(int)attachment.length()];
-						fis = new FileInputStream(attachment);
-						fis.read(datas);
-						data.setDatas(datas);
-						dbInstance.saveObject(data);
+						DBMailAttachment data = new DBMailAttachment();
+						data.setSize(attachment.length());
+						data.setName(attachment.getName());
+						
+						long checksum = FileUtils.checksum(attachment, new Adler32()).getValue();
+						data.setChecksum(new Long(checksum));
+						data.setMimetype(WebappHelper.getMimeType(attachment.getName()));
+						
+						in = new FileInputStream(attachment);
+						String path = saveAttachmentToStorage(data.getName(), data.getMimetype(), checksum, attachment.length(), in);
+						data.setPath(path);
+						data.setMail(mail);
+
+						dbInstance.getCurrentEntityManager().persist(data);
 					} catch (FileNotFoundException e) {
 						logError("File attachment not found: " + attachment, e);
 					} catch (IOException e) {
 						logError("Error with file attachment: " + attachment, e);
 					} finally {
-						IOUtils.closeQuietly(fis);
+						IOUtils.closeQuietly(in);
 					}
 				}
 			}
@@ -1055,9 +1161,9 @@ public class MailManager extends BasicManager {
 						return msg;
 					}
 					messageBodyPart = new MimeBodyPart();
-					
-					DBMailAttachmentData data = getAttachmentWithData(attachment.getKey());
-					DataSource source = new ByteArrayDataSource(data.getDatas(), attachment.getMimetype());
+
+					VFSLeaf data = getAttachmentDatas(attachment);
+					DataSource source = new VFSDataSource(attachment.getName(), attachment.getMimetype(), data);
 					messageBodyPart.setDataHandler(new DataHandler(source));
 					messageBodyPart.setFileName(attachment.getName());
 					multipart.addBodyPart(messageBodyPart);
@@ -1224,5 +1330,37 @@ public class MailManager extends BasicManager {
 			logWarn("Could not send mail", e);
 		}
 	}
+	
+	private static class VFSDataSource implements DataSource {
+		
+		private final String name;
+		private final String contentType;
+		private final VFSLeaf file;
+		
+		public VFSDataSource(String name, String contentType, VFSLeaf file) {
+			this.name = name;
+			this.contentType = contentType;
+			this.file = file;
+		}
+
+		@Override
+		public String getContentType() {
+			return contentType;
+		}
 
+		@Override
+		public InputStream getInputStream() throws IOException {
+			return file.getInputStream();
+		}
+
+		@Override
+		public String getName() {
+			return name;
+		}
+
+		@Override
+		public OutputStream getOutputStream() throws IOException {
+			return null;
+		}
+	}
 }
diff --git a/src/main/java/org/olat/core/util/mail/model/DBMailAttachment.hbm.xml b/src/main/java/org/olat/core/util/mail/model/DBMailAttachment.hbm.xml
index 49956e12f29..7d4dfeb92bb 100644
--- a/src/main/java/org/olat/core/util/mail/model/DBMailAttachment.hbm.xml
+++ b/src/main/java/org/olat/core/util/mail/model/DBMailAttachment.hbm.xml
@@ -12,18 +12,9 @@
 		<property name="size" column="datas_size" type="long"/>
 		<property name="mimetype" column="mimetype" type="string" not-null="false" length="255"/>
 		<property name="name" column="datas_name" type="string" not-null="false" length="255"/>
-		<many-to-one name="mail" column="fk_att_mail_id" class="org.olat.core.util.mail.model.DBMailImpl" fetch="join" unique="false" cascade="none"/>
-  </class>
-  
-  <class name="org.olat.core.util.mail.model.DBMailAttachmentData" table="o_mail_attachment">  
-    <id name="key" column="attachment_id" type="long" unsaved-value="null">
-      <generator class="hilo"/>
-    </id>
-		<property name="creationDate" column="creationdate" type="timestamp" />
-		<property name="datas" column="datas" type="binary" length="16777215"/>
-		<property name="size" column="datas_size" type="long"/>
-		<property name="mimetype" column="mimetype" type="string" not-null="false" length="255"/>
-		<property name="name" column="datas_name" type="string" not-null="false" length="255"/>
+		<property name="checksum" column="datas_checksum" type="long" not-null="false"/>
+		<property name="path" column="datas_path" type="string" not-null="false" length="1024"/>
+		<property name="lastModified" column="datas_lastmodified" type="timestamp" />
 		<many-to-one name="mail" column="fk_att_mail_id" class="org.olat.core.util.mail.model.DBMailImpl" fetch="join" unique="false" cascade="none"/>
   </class>
   
diff --git a/src/main/java/org/olat/core/util/mail/model/DBMailAttachment.java b/src/main/java/org/olat/core/util/mail/model/DBMailAttachment.java
index 19a914d48ac..b688e22c226 100644
--- a/src/main/java/org/olat/core/util/mail/model/DBMailAttachment.java
+++ b/src/main/java/org/olat/core/util/mail/model/DBMailAttachment.java
@@ -19,8 +19,11 @@
  */
 package org.olat.core.util.mail.model;
 
+import java.util.Date;
+
 import org.olat.core.commons.persistence.PersistentObject;
 import org.olat.core.gui.util.CSSHelper;
+import org.olat.core.util.mail.MailAttachment;
 
 /**
  * 
@@ -31,13 +34,16 @@ import org.olat.core.gui.util.CSSHelper;
  *
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  */
-public class DBMailAttachment extends PersistentObject {
+public class DBMailAttachment extends PersistentObject implements MailAttachment {
 
 	private static final long serialVersionUID = -1713863670528439651L;
 
 	private Long size;
 	private String name;
 	private String mimetype;
+	private Long checksum;
+	private String path;
+	private Date lastModified;
 	private DBMailImpl mail;
 	
 	public DBMailAttachment() {
@@ -80,6 +86,30 @@ public class DBMailAttachment extends PersistentObject {
 		this.mimetype = mimetype;
 	}
 	
+	public Long getChecksum() {
+		return checksum;
+	}
+
+	public void setChecksum(Long checksum) {
+		this.checksum = checksum;
+	}
+
+	public String getPath() {
+		return path;
+	}
+
+	public void setPath(String path) {
+		this.path = path;
+	}
+
+	public Date getLastModified() {
+		return lastModified;
+	}
+
+	public void setLastModified(Date lastModified) {
+		this.lastModified = lastModified;
+	}
+
 	@Override
 	public int hashCode() {
 		return getKey() == null ? 2951 : getKey().hashCode();
diff --git a/src/main/java/org/olat/core/util/mail/ui/MailAttachmentMapper.java b/src/main/java/org/olat/core/util/mail/ui/MailAttachmentMapper.java
index ff0c0cc0da4..9e3bfd8fe64 100644
--- a/src/main/java/org/olat/core/util/mail/ui/MailAttachmentMapper.java
+++ b/src/main/java/org/olat/core/util/mail/ui/MailAttachmentMapper.java
@@ -20,19 +20,14 @@
 
 package org.olat.core.util.mail.ui;
 
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-
 import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
 
 import org.olat.core.dispatcher.mapper.Mapper;
 import org.olat.core.gui.media.MediaResource;
 import org.olat.core.gui.media.NotFoundMediaResource;
-import org.olat.core.util.StringHelper;
-import org.olat.core.util.WebappHelper;
 import org.olat.core.util.mail.manager.MailManager;
-import org.olat.core.util.mail.model.DBMailAttachmentData;
+import org.olat.core.util.vfs.VFSLeaf;
+import org.olat.core.util.vfs.VFSMediaResource;
 
 /**
  * 
@@ -62,8 +57,8 @@ public class MailAttachmentMapper implements Mapper {
 				String attachmentKey = relPath.substring(startIndex + ATTACHMENT_CONTEXT.length(), endIndex);
 				try {
 					Long key = new Long(attachmentKey);
-					DBMailAttachmentData datas = mailManager.getAttachmentWithData(key);
-					BytesMediaResource resource = new BytesMediaResource(datas);
+					VFSLeaf datas = mailManager.getAttachmentDatas(key);
+					MediaResource resource = new VFSMediaResource(datas);
 					return resource;	
 				} catch(NumberFormatException e) {
 					return new NotFoundMediaResource(relPath);
@@ -72,55 +67,4 @@ public class MailAttachmentMapper implements Mapper {
 		}
 		return new NotFoundMediaResource(relPath);
 	}
-	
-	public class BytesMediaResource implements MediaResource {
-		
-		private final DBMailAttachmentData datas;
-		
-		public BytesMediaResource(DBMailAttachmentData datas) {
-			this.datas = datas;
-		}
-
-		@Override
-		public String getContentType() {
-			if(StringHelper.containsNonWhitespace(datas.getMimetype())) {
-				return datas.getMimetype();
-			}
-			if(StringHelper.containsNonWhitespace(datas.getName())) {
-				String mimeType = WebappHelper.getMimeType(datas.getName());
-				if(StringHelper.containsNonWhitespace(mimeType)) {
-					return mimeType;
-				}
-			}
-			return "application/octet-stream";
-		}
-
-		@Override
-		public Long getSize() {
-			if(datas.getDatas() == null) return 0l;
-			return new Long(datas.getDatas().length);
-		}
-
-		@Override
-		public InputStream getInputStream() {
-			return new ByteArrayInputStream(datas.getDatas());
-		}
-
-		@Override
-		public Long getLastModified() {
-			return null;
-		}
-
-		@Override
-		public void prepare(HttpServletResponse hres) {
-			String fileName = datas.getName();
-			hres.setHeader("Content-Disposition","filename=\"" + StringHelper.urlEncodeISO88591(fileName) + "\"");
-			hres.setHeader("Content-Description",StringHelper.urlEncodeISO88591(fileName));
-		}
-
-		@Override
-		public void release() {
-			//
-		}
-	}
 }
diff --git a/src/main/java/org/olat/modules/qpool/manager/FileStorage.java b/src/main/java/org/olat/core/util/vfs/FileStorage.java
similarity index 78%
rename from src/main/java/org/olat/modules/qpool/manager/FileStorage.java
rename to src/main/java/org/olat/core/util/vfs/FileStorage.java
index bb4a89d6fe5..de0ba64437d 100644
--- a/src/main/java/org/olat/modules/qpool/manager/FileStorage.java
+++ b/src/main/java/org/olat/core/util/vfs/FileStorage.java
@@ -17,38 +17,31 @@
  * frentix GmbH, http://www.frentix.com
  * <p>
  */
-package org.olat.modules.qpool.manager;
+package org.olat.core.util.vfs;
 
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.UUID;
 
-import org.olat.core.CoreSpringFactory;
 import org.olat.core.logging.OLog;
 import org.olat.core.logging.Tracing;
-import org.olat.core.util.vfs.VFSContainer;
-import org.olat.core.util.vfs.VFSItem;
-import org.olat.core.util.vfs.VFSLeaf;
-import org.olat.modules.qpool.QuestionPoolModule;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
 
 /**
  * 
- * 
- * 
- * Initial date: 07.03.2013<br>
+ * Initial date: 28.05.2013<br>
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  *
  */
-@Service("qpoolFileStorage")
 public class FileStorage {
 	
 	private static final OLog log = Tracing.createLoggerFor(FileStorage.class);
+
+	private VFSContainer rootContainer;
 	
-	@Autowired
-	private QuestionPoolModule qpoolModule;
+	public FileStorage(VFSContainer rootContainer) {
+		this.rootContainer = rootContainer;
+	}
 
 	public String generateDir() {
 		String uuid = UUID.randomUUID().toString();
@@ -70,8 +63,29 @@ public class FileStorage {
 		return path;
 	}
 	
+	public String generateDir(String uuid, boolean addNumberedDir) {
+		if(addNumberedDir) {
+			return generateDir(uuid);
+		}
+
+		String cleanUuid = uuid.replace("-", "");
+		String firstToken = cleanUuid.substring(0, 2);
+		String secondToken = cleanUuid.substring(2, 4);
+		String thirdToken = cleanUuid.substring(4, 6);
+
+		VFSContainer firstContainer = getNextDirectory(rootContainer, firstToken);
+		VFSContainer secondContainer = getNextDirectory(firstContainer, secondToken);
+		getNextDirectory(secondContainer, thirdToken);
+
+		StringBuilder sb = new StringBuilder();
+		sb.append(firstToken).append("/")
+		  .append(secondToken).append("/")
+		  .append(thirdToken).append("/");
+		String path = sb.toString();
+		return path;
+	}
+	
 	protected String createContainer(String firstToken, String secondToken, String thirdToken) {
-		VFSContainer rootContainer = qpoolModule.getRootContainer();
 		VFSContainer firstContainer = getNextDirectory(rootContainer, firstToken);
 		VFSContainer secondContainer = getNextDirectory(firstContainer, secondToken);
 		VFSContainer thirdContainer = getNextDirectory(secondContainer, thirdToken);
@@ -94,6 +108,7 @@ public class FileStorage {
 				}
 				if(!names.contains(potentielName)) {
 					lastToken = potentielName;
+					break;
 				}
 			}
 		}
@@ -106,16 +121,12 @@ public class FileStorage {
 	}
 	
 	public VFSContainer getContainer(String dir) {
-		VFSContainer rootContainer = CoreSpringFactory.getImpl(QuestionPoolModule.class).getRootContainer();
-		String[] tokens = dir.split("/");
-		String firstToken = tokens[0];
-		VFSContainer firstContainer = getNextDirectory(rootContainer, firstToken);
-		String secondToken = tokens[1];
-		VFSContainer secondContainer = getNextDirectory(firstContainer, secondToken);
-		String thirdToken = tokens[2];
-		VFSContainer thridContainer = getNextDirectory(secondContainer, thirdToken);
-		String forthToken = tokens[3];
-		return getNextDirectory(thridContainer, forthToken);
+		String[] tokens = dir.split("/");		
+		VFSContainer container = rootContainer;
+		for(String token:tokens) {
+			container = getNextDirectory(container, token);
+		}
+		return container;
 	}
 	
 	private VFSContainer getNextDirectory(VFSContainer container, String token) {
diff --git a/src/main/java/org/olat/ims/qti/qpool/QTIExportProcessor.java b/src/main/java/org/olat/ims/qti/qpool/QTIExportProcessor.java
index bfdea485676..6b0fc04c054 100644
--- a/src/main/java/org/olat/ims/qti/qpool/QTIExportProcessor.java
+++ b/src/main/java/org/olat/ims/qti/qpool/QTIExportProcessor.java
@@ -54,7 +54,7 @@ import org.olat.core.util.xml.XMLParser;
 import org.olat.ims.qti.QTIConstants;
 import org.olat.ims.resources.IMSEntityResolver;
 import org.olat.modules.qpool.QuestionItemFull;
-import org.olat.modules.qpool.manager.FileStorage;
+import org.olat.modules.qpool.manager.QPoolFileStorage;
 import org.xml.sax.Attributes;
 import org.xml.sax.InputSource;
 import org.xml.sax.helpers.DefaultHandler;
@@ -69,9 +69,9 @@ public class QTIExportProcessor {
 	
 	private static final OLog log = Tracing.createLoggerFor(QTIExportProcessor.class);
 
-	private final FileStorage qpoolFileStorage;
+	private final QPoolFileStorage qpoolFileStorage;
 	
-	public QTIExportProcessor(FileStorage qpoolFileStorage) {
+	public QTIExportProcessor(QPoolFileStorage qpoolFileStorage) {
 		this.qpoolFileStorage = qpoolFileStorage;
 	}
 	
diff --git a/src/main/java/org/olat/ims/qti/qpool/QTIImportProcessor.java b/src/main/java/org/olat/ims/qti/qpool/QTIImportProcessor.java
index b3547e9a058..cf653b5fd80 100644
--- a/src/main/java/org/olat/ims/qti/qpool/QTIImportProcessor.java
+++ b/src/main/java/org/olat/ims/qti/qpool/QTIImportProcessor.java
@@ -56,7 +56,7 @@ import org.olat.ims.qti.editor.beecom.parser.ItemParser;
 import org.olat.ims.resources.IMSEntityResolver;
 import org.olat.modules.qpool.QuestionItem;
 import org.olat.modules.qpool.QuestionType;
-import org.olat.modules.qpool.manager.FileStorage;
+import org.olat.modules.qpool.manager.QPoolFileStorage;
 import org.olat.modules.qpool.manager.QEducationalContextDAO;
 import org.olat.modules.qpool.manager.QItemTypeDAO;
 import org.olat.modules.qpool.manager.QuestionItemDAO;
@@ -85,18 +85,18 @@ class QTIImportProcessor {
 	private final File importedFile;
 
 	private final QItemTypeDAO qItemTypeDao;
-	private final FileStorage qpoolFileStorage;
+	private final QPoolFileStorage qpoolFileStorage;
 	private final QuestionItemDAO questionItemDao;
 	private final QEducationalContextDAO qEduContextDao;
 	
 	public QTIImportProcessor(Identity owner, Locale defaultLocale, QuestionItemDAO questionItemDao,
-			QItemTypeDAO qItemTypeDao, QEducationalContextDAO qEduContextDao, FileStorage qpoolFileStorage) {
+			QItemTypeDAO qItemTypeDao, QEducationalContextDAO qEduContextDao, QPoolFileStorage qpoolFileStorage) {
 		this(owner, defaultLocale, null, null, questionItemDao, qItemTypeDao, qEduContextDao, qpoolFileStorage);
 	}
 
 	public QTIImportProcessor(Identity owner, Locale defaultLocale, String importedFilename, File importedFile,
 			QuestionItemDAO questionItemDao, QItemTypeDAO qItemTypeDao, QEducationalContextDAO qEduContextDao,
-			FileStorage qpoolFileStorage) {
+			QPoolFileStorage qpoolFileStorage) {
 		this.owner = owner;
 		this.defaultLocale = defaultLocale;
 		this.importedFilename = importedFilename;
diff --git a/src/main/java/org/olat/ims/qti/qpool/QTIQPoolServiceProvider.java b/src/main/java/org/olat/ims/qti/qpool/QTIQPoolServiceProvider.java
index d3edc234731..c97838bb8d2 100644
--- a/src/main/java/org/olat/ims/qti/qpool/QTIQPoolServiceProvider.java
+++ b/src/main/java/org/olat/ims/qti/qpool/QTIQPoolServiceProvider.java
@@ -58,7 +58,7 @@ import org.olat.modules.qpool.QuestionItem;
 import org.olat.modules.qpool.QuestionItemFull;
 import org.olat.modules.qpool.QuestionItemShort;
 import org.olat.modules.qpool.ExportFormatOptions.Outcome;
-import org.olat.modules.qpool.manager.FileStorage;
+import org.olat.modules.qpool.manager.QPoolFileStorage;
 import org.olat.modules.qpool.manager.QEducationalContextDAO;
 import org.olat.modules.qpool.manager.QItemTypeDAO;
 import org.olat.modules.qpool.manager.QuestionItemDAO;
@@ -84,7 +84,7 @@ public class QTIQPoolServiceProvider implements QPoolSPI {
 	public static final String QTI_12_OO_TEST = "OpenOLAT Test";
 
 	@Autowired
-	private FileStorage qpoolFileStorage;
+	private QPoolFileStorage qpoolFileStorage;
 	@Autowired
 	private QItemTypeDAO qItemTypeDao;
 	@Autowired
diff --git a/src/main/java/org/olat/modules/qpool/impl/FileQPoolServiceProvider.java b/src/main/java/org/olat/modules/qpool/impl/FileQPoolServiceProvider.java
index ebbde31f193..b17c271b69f 100644
--- a/src/main/java/org/olat/modules/qpool/impl/FileQPoolServiceProvider.java
+++ b/src/main/java/org/olat/modules/qpool/impl/FileQPoolServiceProvider.java
@@ -29,7 +29,7 @@ import org.olat.modules.qpool.QPoolService;
 import org.olat.modules.qpool.QuestionItem;
 import org.olat.modules.qpool.QuestionType;
 import org.olat.modules.qpool.manager.AbstractQPoolServiceProvider;
-import org.olat.modules.qpool.manager.FileStorage;
+import org.olat.modules.qpool.manager.QPoolFileStorage;
 import org.olat.modules.qpool.model.QItemType;
 import org.olat.modules.qpool.ui.FilePreviewController;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -48,10 +48,10 @@ public class FileQPoolServiceProvider extends AbstractQPoolServiceProvider {
 	@Autowired
 	private QPoolService qpoolService;
 	@Autowired
-	private FileStorage qpoolFileStorage;
+	private QPoolFileStorage qpoolFileStorage;
 
 	@Override
-	public FileStorage getFileStorage() {
+	public QPoolFileStorage getFileStorage() {
 		return qpoolFileStorage;
 	}
 
diff --git a/src/main/java/org/olat/modules/qpool/impl/TextQPoolServiceProvider.java b/src/main/java/org/olat/modules/qpool/impl/TextQPoolServiceProvider.java
index 6bb4001a746..d9720e7326d 100644
--- a/src/main/java/org/olat/modules/qpool/impl/TextQPoolServiceProvider.java
+++ b/src/main/java/org/olat/modules/qpool/impl/TextQPoolServiceProvider.java
@@ -29,7 +29,7 @@ import org.olat.modules.qpool.QPoolService;
 import org.olat.modules.qpool.QuestionItem;
 import org.olat.modules.qpool.QuestionType;
 import org.olat.modules.qpool.manager.AbstractQPoolServiceProvider;
-import org.olat.modules.qpool.manager.FileStorage;
+import org.olat.modules.qpool.manager.QPoolFileStorage;
 import org.olat.modules.qpool.model.QItemType;
 import org.olat.modules.qpool.ui.TextPreviewController;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -48,10 +48,10 @@ public class TextQPoolServiceProvider extends AbstractQPoolServiceProvider {
 	@Autowired
 	private QPoolService qpoolService;
 	@Autowired
-	private FileStorage qpoolFileStorage;
+	private QPoolFileStorage qpoolFileStorage;
 	
 	@Override
-	public FileStorage getFileStorage() {
+	public QPoolFileStorage getFileStorage() {
 		return qpoolFileStorage;
 	}
 	
diff --git a/src/main/java/org/olat/modules/qpool/manager/AbstractQPoolServiceProvider.java b/src/main/java/org/olat/modules/qpool/manager/AbstractQPoolServiceProvider.java
index ddc54fc72ee..3f2ad1f2570 100644
--- a/src/main/java/org/olat/modules/qpool/manager/AbstractQPoolServiceProvider.java
+++ b/src/main/java/org/olat/modules/qpool/manager/AbstractQPoolServiceProvider.java
@@ -68,7 +68,7 @@ public abstract class AbstractQPoolServiceProvider implements QPoolSPI {
 	
 	private static final OLog log = Tracing.createLoggerFor(AbstractQPoolServiceProvider.class);
 	
-	public abstract FileStorage getFileStorage();
+	public abstract QPoolFileStorage getFileStorage();
 	
 	public abstract QItemType getDefaultType();
 	
diff --git a/src/main/java/org/olat/modules/qpool/manager/QPoolFileStorage.java b/src/main/java/org/olat/modules/qpool/manager/QPoolFileStorage.java
new file mode 100644
index 00000000000..866762e81db
--- /dev/null
+++ b/src/main/java/org/olat/modules/qpool/manager/QPoolFileStorage.java
@@ -0,0 +1,63 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.modules.qpool.manager;
+
+import javax.annotation.PostConstruct;
+
+import org.olat.core.util.vfs.FileStorage;
+import org.olat.core.util.vfs.VFSContainer;
+import org.olat.modules.qpool.QuestionPoolModule;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 
+ * 
+ * 
+ * Initial date: 07.03.2013<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+@Service("qpoolFileStorage")
+public class QPoolFileStorage {
+
+	@Autowired
+	private QuestionPoolModule qpoolModule;
+	
+	private FileStorage fileStorage;
+	
+	@PostConstruct
+	public void init() {
+		VFSContainer rootContainer = qpoolModule.getRootContainer();
+		fileStorage = new FileStorage(rootContainer);
+	}
+
+	public String generateDir() {
+		return fileStorage.generateDir();
+	}
+	
+	public String generateDir(String uuid) {
+		return fileStorage.generateDir(uuid);
+	}
+
+	public VFSContainer getContainer(String dir) {
+		return fileStorage.getContainer(dir);
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/olat/modules/qpool/manager/QuestionItemDAO.java b/src/main/java/org/olat/modules/qpool/manager/QuestionItemDAO.java
index 48c20ebf191..72d675c8929 100644
--- a/src/main/java/org/olat/modules/qpool/manager/QuestionItemDAO.java
+++ b/src/main/java/org/olat/modules/qpool/manager/QuestionItemDAO.java
@@ -66,7 +66,7 @@ public class QuestionItemDAO {
 	@Autowired
 	private DB dbInstance;
 	@Autowired
-	private FileStorage qpoolFileStorage;
+	private QPoolFileStorage qpoolFileStorage;
 	@Autowired
 	private BaseSecurity securityManager;
 	
diff --git a/src/main/java/org/olat/upgrade/OLATUpgrade_9_0_0.java b/src/main/java/org/olat/upgrade/OLATUpgrade_9_0_0.java
new file mode 100644
index 00000000000..8b8edf0ed84
--- /dev/null
+++ b/src/main/java/org/olat/upgrade/OLATUpgrade_9_0_0.java
@@ -0,0 +1,145 @@
+package org.olat.upgrade;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Date;
+import java.util.List;
+import java.util.zip.Adler32;
+import java.util.zip.CheckedInputStream;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.output.NullOutputStream;
+import org.olat.core.commons.persistence.DB;
+import org.olat.core.logging.OLog;
+import org.olat.core.logging.Tracing;
+import org.olat.core.util.mail.manager.MailManager;
+import org.olat.core.util.vfs.VFSLeaf;
+import org.olat.upgrade.model.DBMailAttachmentData;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * Initial date: 28.05.2013<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class OLATUpgrade_9_0_0 extends OLATUpgrade {
+	
+	private static final OLog log = Tracing.createLoggerFor(OLATUpgrade_9_0_0.class);
+	
+	private static final int BATCH_SIZE = 20;
+	private static final String TASK_MAILS = "Upgrade mails";
+	private static final String VERSION = "OLAT_9.0.0";
+	
+	@Autowired
+	private DB dbInstance;
+	@Autowired
+	private MailManager mailManager;
+	
+	public OLATUpgrade_9_0_0() {
+		super();
+	}
+
+	@Override
+	public String getVersion() {
+		return VERSION;
+	}
+	
+	@Override
+	public boolean doPreSystemInitUpgrade(UpgradeManager upgradeManager) {
+		return false;
+	}
+
+	@Override
+	public boolean doPostSystemInitUpgrade(UpgradeManager upgradeManager) {
+		UpgradeHistoryData uhd = upgradeManager.getUpgradesHistory(VERSION);
+		if (uhd == null) {
+			// has never been called, initialize
+			uhd = new UpgradeHistoryData();
+		} else {
+			if (uhd.isInstallationComplete()) {
+				return false;
+			}
+		}
+		
+		boolean allOk = upgradeMailAttachments(upgradeManager, uhd);
+		
+		uhd.setInstallationComplete(allOk);
+		upgradeManager.setUpgradesHistory(uhd, VERSION);
+		if(allOk) {
+			log.audit("Finished OLATUpgrade_9_0_0 successfully!");
+		} else {
+			log.audit("OLATUpgrade_9_0_0 not finished, try to restart OpenOLAT!");
+		}
+		return allOk;
+	}
+	
+	private boolean upgradeMailAttachments(UpgradeManager upgradeManager, UpgradeHistoryData uhd) {
+		if (!uhd.getBooleanDataValue(TASK_MAILS)) {
+			int counter = 0;
+			List<Long> attachments;
+			do {
+				attachments = findAttachments(counter, BATCH_SIZE);
+				for(Long attachment:attachments) {
+					processAttachments(attachment);
+				}
+				counter += attachments.size();
+				log.audit("Mail attachment processed: " + attachments.size());
+				dbInstance.commitAndCloseSession();
+			} while(attachments.size() == BATCH_SIZE);
+			uhd.setBooleanDataValue(TASK_MAILS, true);
+			upgradeManager.setUpgradesHistory(uhd, VERSION);
+		}
+		return true;
+	}
+	
+	private List<Long> findAttachments(int firstResult, int maxResults) {
+		StringBuilder sb = new StringBuilder();	
+		sb.append("select attachment.key from ").append(DBMailAttachmentData.class.getName()).append(" attachment order by key");
+		return dbInstance.getCurrentEntityManager().createQuery(sb.toString(), Long.class)
+				.setFirstResult(firstResult)
+				.setMaxResults(maxResults)
+				.getResultList();
+	}
+	
+	private void processAttachments(Long attachment) {
+		try {
+			DBMailAttachmentData data = dbInstance.getCurrentEntityManager()
+					.find(DBMailAttachmentData.class, attachment);
+			
+			byte[] binaryDatas = data.getDatas();
+			if(binaryDatas == null || binaryDatas.length <= 0) {
+				return;
+			}
+			
+			long checksum = checksum(binaryDatas);
+			String name = data.getName();
+			InputStream in = new ByteArrayInputStream(binaryDatas);
+			String path = mailManager.saveAttachmentToStorage(name, data.getMimetype(), checksum, data.getSize(), in);
+			data.setChecksum(new Long(checksum));
+			data.setLastModified(new Date());
+			data.setPath(path);
+			
+			VFSLeaf savedFile = mailManager.getAttachmentDatas(data);
+			if(savedFile != null && savedFile.exists() && savedFile.getSize() > 0) {
+				data.setDatas(null);
+				dbInstance.getCurrentEntityManager().merge(data);
+			}
+		} catch (IOException e) {
+			log.error("", e);
+		}
+	}
+	
+	private long checksum(byte[] binaryDatas) throws IOException {
+		InputStream in = null;
+		Adler32 checksum = new Adler32();
+    try {
+        in = new CheckedInputStream(new ByteArrayInputStream(binaryDatas), checksum);
+        IOUtils.copy(in, new NullOutputStream());
+    } finally {
+        IOUtils.closeQuietly(in);
+    }
+    return checksum.getValue();
+	}
+}
diff --git a/src/main/java/org/olat/upgrade/_spring/upgradeContext.xml b/src/main/java/org/olat/upgrade/_spring/upgradeContext.xml
index 3ed8c9c39d2..f58c4d75d54 100644
--- a/src/main/java/org/olat/upgrade/_spring/upgradeContext.xml
+++ b/src/main/java/org/olat/upgrade/_spring/upgradeContext.xml
@@ -37,6 +37,7 @@
 				<bean id="upgrade_8_2_0" class="org.olat.upgrade.OLATUpgrade_8_2_0"/>
 				<bean id="upgrade_8_3_0" class="org.olat.upgrade.OLATUpgrade_8_3_0"/>
 				<bean id="upgrade_8_4_0" class="org.olat.upgrade.OLATUpgrade_8_4_0"/>
+				<bean id="upgrade_9_0_0" class="org.olat.upgrade.OLATUpgrade_9_0_0"/>
 			</list>
 		</property>
 	</bean>
diff --git a/src/main/java/org/olat/upgrade/model/DBMailAttachmentData.hbm.xml b/src/main/java/org/olat/upgrade/model/DBMailAttachmentData.hbm.xml
new file mode 100644
index 00000000000..dcfd4850022
--- /dev/null
+++ b/src/main/java/org/olat/upgrade/model/DBMailAttachmentData.hbm.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<!DOCTYPE hibernate-mapping PUBLIC 
+        "-//Hibernate/Hibernate Mapping DTD//EN"
+        "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
+<hibernate-mapping default-lazy="false">
+  
+  <class name="org.olat.upgrade.model.DBMailAttachmentData" table="o_mail_attachment">  
+    <id name="key" column="attachment_id" type="long" unsaved-value="null">
+      <generator class="hilo"/>
+    </id>
+		<property name="creationDate" column="creationdate" type="timestamp" />
+		<property name="datas" column="datas" type="binary" length="16777215"/>
+		<property name="size" column="datas_size" type="long"/>
+		<property name="mimetype" column="mimetype" type="string" not-null="false" length="255"/>
+		<property name="name" column="datas_name" type="string" not-null="false" length="255"/>
+		<property name="checksum" column="datas_checksum" type="long" not-null="false"/>
+		<property name="path" column="datas_path" type="string" not-null="false" length="1024"/>
+		<property name="lastModified" column="datas_lastmodified" type="timestamp" />
+		<many-to-one name="mail" column="fk_att_mail_id" class="org.olat.core.util.mail.model.DBMailImpl" fetch="join" unique="false" cascade="none"/>
+  </class>
+  
+</hibernate-mapping>
diff --git a/src/main/java/org/olat/core/util/mail/model/DBMailAttachmentData.java b/src/main/java/org/olat/upgrade/model/DBMailAttachmentData.java
similarity index 76%
rename from src/main/java/org/olat/core/util/mail/model/DBMailAttachmentData.java
rename to src/main/java/org/olat/upgrade/model/DBMailAttachmentData.java
index 9c9854eab40..691191fe7e8 100644
--- a/src/main/java/org/olat/core/util/mail/model/DBMailAttachmentData.java
+++ b/src/main/java/org/olat/upgrade/model/DBMailAttachmentData.java
@@ -17,9 +17,13 @@
  * frentix GmbH, http://www.frentix.com
  * <p>
  */
-package org.olat.core.util.mail.model;
+package org.olat.upgrade.model;
+
+import java.util.Date;
 
 import org.olat.core.commons.persistence.PersistentObject;
+import org.olat.core.util.mail.MailAttachment;
+import org.olat.core.util.mail.model.DBMailImpl;
 
 /**
  * 
@@ -30,13 +34,16 @@ import org.olat.core.commons.persistence.PersistentObject;
  *
  * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
  */
-public class DBMailAttachmentData extends PersistentObject {
+public class DBMailAttachmentData extends PersistentObject implements MailAttachment {
 
 	private static final long serialVersionUID = -3741636430048220733L;
 	
 	private Long size;
 	private String name;
 	private String mimetype;
+	private Long checksum;
+	private String path;
+	private Date lastModified;
 	private byte[] datas;
 	private DBMailImpl mail;
 	
@@ -84,6 +91,30 @@ public class DBMailAttachmentData extends PersistentObject {
 		this.mimetype = mimetype;
 	}
 
+	public Long getChecksum() {
+		return checksum;
+	}
+
+	public void setChecksum(Long checksum) {
+		this.checksum = checksum;
+	}
+
+	public String getPath() {
+		return path;
+	}
+
+	public void setPath(String path) {
+		this.path = path;
+	}
+
+	public Date getLastModified() {
+		return lastModified;
+	}
+
+	public void setLastModified(Date lastModified) {
+		this.lastModified = lastModified;
+	}
+
 	@Override
 	public int hashCode() {
 		return getKey() == null ? 921536 : getKey().intValue();
diff --git a/src/main/resources/database/mysql/alter_8_4_0_to_9_0_0.sql b/src/main/resources/database/mysql/alter_8_4_0_to_9_0_0.sql
index 6bff1cff0ab..5d97dd8569d 100644
--- a/src/main/resources/database/mysql/alter_8_4_0_to_9_0_0.sql
+++ b/src/main/resources/database/mysql/alter_8_4_0_to_9_0_0.sql
@@ -342,4 +342,14 @@ alter table o_lti_outcome add constraint idx_lti_outcome_rsrc_id foreign key (fk
 -- mapper
 alter table o_mapper add column expirationdate datetime;
 
+-- mail
+alter table o_mail_attachment add column datas_checksum bigint;
+alter table o_mail_attachment add column datas_path varchar(1024);
+alter table o_mail_attachment add column datas_lastmodified datetime;
+create index idx_mail_att_checksum_idx on o_mail_attachment (datas_checksum);
+create index idx_mail_path_idx on o_mail_attachment (datas_path);
+create index idx_mail_att_siblings_idx on o_mail_attachment (datas_checksum, mimetype, datas_size, datas_name);
+
+
+
 
diff --git a/src/main/resources/database/mysql/setupDatabase.sql b/src/main/resources/database/mysql/setupDatabase.sql
index fa5c5416432..d45f194c39b 100644
--- a/src/main/resources/database/mysql/setupDatabase.sql
+++ b/src/main/resources/database/mysql/setupDatabase.sql
@@ -822,14 +822,17 @@ create table if not exists o_mail_recipient (
 
 -- mail attachments
 create table o_mail_attachment (
-	attachment_id bigint NOT NULL,
-  creationdate datetime,
-	datas mediumblob,
-	datas_size bigint,
-	datas_name varchar(255),
-	mimetype varchar(255),
-  fk_att_mail_id bigint,
-	primary key (attachment_id)
+   attachment_id bigint NOT NULL,
+   creationdate datetime,
+   datas mediumblob,
+   datas_size bigint,
+   datas_name varchar(255),
+   datas_checksum bigint,
+   datas_path varchar(1024),
+   datas_lastmodified datetime,
+   mimetype varchar(255),
+   fk_att_mail_id bigint,
+   primary key (attachment_id)
 );
 
 -- access control
@@ -1988,6 +1991,9 @@ alter table o_mail_recipient add constraint FKF86663165A4FA5DG foreign key (fk_r
 alter table o_mail add constraint FKF86663165A4FA5DC foreign key (fk_from_id) references o_mail_recipient (recipient_id);
 alter table o_mail_to_recipient add constraint FKF86663165A4FA5DD foreign key (fk_recipient_id) references o_mail_recipient (recipient_id);
 alter table o_mail_attachment add constraint FKF86663165A4FA5DF foreign key (fk_att_mail_id) references o_mail (mail_id);
+create index idx_mail_att_checksum_idx on o_mail_attachment (datas_checksum);
+create index idx_mail_path_idx on o_mail_attachment (datas_path);
+create index idx_mail_att_siblings_idx on o_mail_attachment (datas_checksum, mimetype, datas_size, datas_name);
 
 create index ac_offer_to_resource_idx on o_ac_offer (fk_resource_id);
 alter table o_ac_offer_access add constraint off_to_meth_meth_ctx foreign key (fk_method_id) references o_ac_method (method_id);
diff --git a/src/main/resources/database/oracle/alter_8_4_0_to_9_0_0.sql b/src/main/resources/database/oracle/alter_8_4_0_to_9_0_0.sql
index 4db7f9325c0..954111f7b0f 100644
--- a/src/main/resources/database/oracle/alter_8_4_0_to_9_0_0.sql
+++ b/src/main/resources/database/oracle/alter_8_4_0_to_9_0_0.sql
@@ -341,3 +341,12 @@ create index idx_lti_outcome_rsrc_id_idx on o_lti_outcome (fk_resource_id);
 
 -- mapper
 alter table o_mapper add (expirationdate date);
+
+-- mail
+alter table o_mail_attachment add (datas_checksum number(20));
+alter table o_mail_attachment add (datas_path varchar2(1024 char));
+alter table o_mail_attachment add (datas_lastmodified date);
+create index idx_mail_att_checksum_idx on o_mail_attachment (datas_checksum);
+create index idx_mail_path_idx on o_mail_attachment (datas_path);
+create index idx_mail_att_siblings_idx on o_mail_attachment (datas_checksum, mimetype, datas_size, datas_name);
+
diff --git a/src/main/resources/database/oracle/setupDatabase.sql b/src/main/resources/database/oracle/setupDatabase.sql
index e57b554e256..fa73f65f13b 100644
--- a/src/main/resources/database/oracle/setupDatabase.sql
+++ b/src/main/resources/database/oracle/setupDatabase.sql
@@ -762,6 +762,9 @@ create table o_mail_attachment (
   datas_size number(20),
   datas_name varchar(255 char),
   mimetype varchar(255 char),
+  datas_checksum number(20),
+  datas_path varchar2(1024 char),
+  datas_lastmodified date,
   fk_att_mail_id number(20),
   primary key (attachment_id)
 );
@@ -2036,6 +2039,9 @@ alter table o_mail_recipient add constraint FKF86663165A4FA5DG foreign key (fk_r
 alter table o_mail add constraint FKF86663165A4FA5DC foreign key (fk_from_id) references o_mail_recipient (recipient_id);
 alter table o_mail_to_recipient add constraint FKF86663165A4FA5DD foreign key (fk_recipient_id) references o_mail_recipient (recipient_id);
 alter table o_mail_attachment add constraint FKF86663165A4FA5DF foreign key (fk_att_mail_id) references o_mail (mail_id);
+create index idx_mail_att_checksum_idx on o_mail_attachment (datas_checksum);
+create index idx_mail_path_idx on o_mail_attachment (datas_path);
+create index idx_mail_att_siblings_idx on o_mail_attachment (datas_checksum, mimetype, datas_size, datas_name);
 
 create index ac_offer_to_resource_idx on o_ac_offer (fk_resource_id);
 alter table o_ac_offer_access add constraint off_to_meth_meth_ctx foreign key (fk_method_id) references o_ac_method (method_id);
diff --git a/src/main/resources/database/postgresql/alter_8_4_0_to_9_0_0.sql b/src/main/resources/database/postgresql/alter_8_4_0_to_9_0_0.sql
index d66131ae633..fc417de44f7 100644
--- a/src/main/resources/database/postgresql/alter_8_4_0_to_9_0_0.sql
+++ b/src/main/resources/database/postgresql/alter_8_4_0_to_9_0_0.sql
@@ -344,4 +344,12 @@ create index idx_lti_outcome_rsrc_id_idx on o_lti_outcome (fk_resource_id);
 -- mapper
 alter table o_mapper add column expirationdate timestamp;
 
+-- mail
+alter table o_mail_attachment add column datas_checksum int8;
+alter table o_mail_attachment add column datas_path varchar(1024);
+alter table o_mail_attachment add column datas_lastmodified timestamp;
+create index idx_mail_att_checksum_idx on o_mail_attachment (datas_checksum);
+create index idx_mail_path_idx on o_mail_attachment (datas_path);
+create index idx_mail_att_siblings_idx on o_mail_attachment (datas_checksum, mimetype, datas_size, datas_name);
+
 
diff --git a/src/main/resources/database/postgresql/setupDatabase.sql b/src/main/resources/database/postgresql/setupDatabase.sql
index f0a9dfeee26..98fb9424e10 100644
--- a/src/main/resources/database/postgresql/setupDatabase.sql
+++ b/src/main/resources/database/postgresql/setupDatabase.sql
@@ -692,14 +692,17 @@ create table o_mail_recipient (
 
 -- mail attachments
 create table o_mail_attachment (
-	attachment_id int8 NOT NULL,
-  creationdate timestamp,
-	datas bytea,
-	datas_size int8,
-	datas_name varchar(255),
-	mimetype varchar(255),
-  fk_att_mail_id int8,
-	primary key (attachment_id)
+   attachment_id int8 NOT NULL,
+   creationdate timestamp,
+   datas bytea,
+   datas_size int8,
+   datas_name varchar(255),
+   datas_checksum int8,
+   datas_path varchar(1024),
+   datas_lastmodified timestamp,
+   mimetype varchar(255),
+   fk_att_mail_id int8,
+   primary key (attachment_id)
 );
 
 -- access control
@@ -1921,6 +1924,9 @@ alter table o_mail_recipient add constraint FKF86663165A4FA5DG foreign key (fk_r
 alter table o_mail add constraint FKF86663165A4FA5DC foreign key (fk_from_id) references o_mail_recipient (recipient_id);
 alter table o_mail_to_recipient add constraint FKF86663165A4FA5DD foreign key (fk_recipient_id) references o_mail_recipient (recipient_id);
 alter table o_mail_attachment add constraint FKF86663165A4FA5DF foreign key (fk_att_mail_id) references o_mail (mail_id);
+create index idx_mail_att_checksum_idx on o_mail_attachment (datas_checksum);
+create index idx_mail_path_idx on o_mail_attachment (datas_path);
+create index idx_mail_att_siblings_idx on o_mail_attachment (datas_checksum, mimetype, datas_size, datas_name);
 
 create index ac_offer_to_resource_idx on o_ac_offer (fk_resource_id);
 alter table o_ac_offer_access add constraint off_to_meth_meth_ctx foreign key (fk_method_id) references o_ac_method (method_id);
diff --git a/src/test/java/org/olat/ims/qti/qpool/QTIExportProcessorTest.java b/src/test/java/org/olat/ims/qti/qpool/QTIExportProcessorTest.java
index b46a4e386cb..c34cd93ded8 100644
--- a/src/test/java/org/olat/ims/qti/qpool/QTIExportProcessorTest.java
+++ b/src/test/java/org/olat/ims/qti/qpool/QTIExportProcessorTest.java
@@ -40,7 +40,7 @@ import org.olat.core.commons.persistence.DB;
 import org.olat.core.id.Identity;
 import org.olat.modules.qpool.QuestionItem;
 import org.olat.modules.qpool.QuestionItemFull;
-import org.olat.modules.qpool.manager.FileStorage;
+import org.olat.modules.qpool.manager.QPoolFileStorage;
 import org.olat.modules.qpool.manager.QEducationalContextDAO;
 import org.olat.modules.qpool.manager.QItemTypeDAO;
 import org.olat.modules.qpool.manager.QuestionItemDAO;
@@ -61,7 +61,7 @@ public class QTIExportProcessorTest extends OlatTestCase {
 	@Autowired
 	private DB dbInstance;
 	@Autowired
-	private FileStorage qpoolFileStorage;
+	private QPoolFileStorage qpoolFileStorage;
 	@Autowired
 	private QItemTypeDAO qItemTypeDao;
 	@Autowired
diff --git a/src/test/java/org/olat/ims/qti/qpool/QTIImportProcessorTest.java b/src/test/java/org/olat/ims/qti/qpool/QTIImportProcessorTest.java
index e8498221232..850d51dc4e4 100644
--- a/src/test/java/org/olat/ims/qti/qpool/QTIImportProcessorTest.java
+++ b/src/test/java/org/olat/ims/qti/qpool/QTIImportProcessorTest.java
@@ -48,7 +48,7 @@ import org.olat.modules.qpool.QuestionItem;
 import org.olat.modules.qpool.QuestionItemFull;
 import org.olat.modules.qpool.QuestionStatus;
 import org.olat.modules.qpool.QuestionType;
-import org.olat.modules.qpool.manager.FileStorage;
+import org.olat.modules.qpool.manager.QPoolFileStorage;
 import org.olat.modules.qpool.manager.QEducationalContextDAO;
 import org.olat.modules.qpool.manager.QItemTypeDAO;
 import org.olat.modules.qpool.manager.QuestionItemDAO;
@@ -72,7 +72,7 @@ public class QTIImportProcessorTest extends OlatTestCase {
 	@Autowired
 	private DB dbInstance;
 	@Autowired
-	private FileStorage qpoolFileStorage;
+	private QPoolFileStorage qpoolFileStorage;
 	@Autowired
 	private QItemTypeDAO qItemTypeDao;
 	@Autowired
diff --git a/src/test/java/org/olat/modules/qpool/manager/FileStorageTest.java b/src/test/java/org/olat/modules/qpool/manager/FileStorageTest.java
index 44fb6c047a7..3cc21bd7e32 100644
--- a/src/test/java/org/olat/modules/qpool/manager/FileStorageTest.java
+++ b/src/test/java/org/olat/modules/qpool/manager/FileStorageTest.java
@@ -37,7 +37,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 public class FileStorageTest extends OlatTestCase {
 	
 	@Autowired
-	private FileStorage qpoolFileStorage;
+	private QPoolFileStorage qpoolFileStorage;
 	
 	@Test
 	public void testGenerateDir() {
-- 
GitLab