Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* ***** BEGIN LICENSE BLOCK *****
* Zimbra Collaboration Suite, Network Edition.
* Copyright (C) 2026 Zimbra, Inc. All Rights Reserved.
* ***** END LICENSE BLOCK *****
*/

package com.zimbra.cs.store;

import com.zimbra.common.service.ServiceException;
import com.zimbra.cs.store.file.FileBlobStore;
import com.zimbra.cs.volume.Volume;
import com.zimbra.cs.volume.VolumeManager;
import java.io.File;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.verify;
import static org.powermock.api.mockito.PowerMockito.when;

@RunWith(PowerMockRunner.class)
@PrepareForTest({FileBlobStore.class, Volume.class, VolumeManager.class})
public class FileBlobStoreMethodTest {

private VolumeManager manager;

private Volume volume;

private IncomingDirectory incomingDirectory;

private FileBlobStore fileBlobStore;

@Before
public void setUp() {
volume = PowerMockito.mock(Volume.class);
incomingDirectory = PowerMockito.mock(IncomingDirectory.class);
fileBlobStore = new FileBlobStore();
PowerMockito.suppress(
PowerMockito.method(FileBlobStore.class, "ensureDirExists")
);
}

@Test
public void testGetBlobBuilderWithVolume() throws Exception {
File file = new File("testFile");
when(volume.getIncomingDirectory()).thenReturn(incomingDirectory);
when(incomingDirectory.getNewIncomingFile()).thenReturn(file);
when(volume.getId()).thenReturn((short) 1);
BlobBuilder builder = fileBlobStore.getBlobBuilder(volume);
assertNotNull(builder);
}

@Test
public void testGetBlobBuilderVolumeNullUsesManager() throws Exception {
VolumeManager mockManager = PowerMockito.mock(VolumeManager.class);
Volume defaultVolume = PowerMockito.mock(Volume.class);
IncomingDirectory dir = PowerMockito.mock(IncomingDirectory.class);
File file = new File("testFile");
when(mockManager.getCurrentMessageVolume()).thenReturn(defaultVolume);
when(defaultVolume.getIncomingDirectory()).thenReturn(dir);
when(dir.getNewIncomingFile()).thenReturn(file);
when(defaultVolume.getId()).thenReturn((short) 1);
PowerMockito.field(FileBlobStore.class, "MANAGER").set(null, mockManager);
BlobBuilder builder = fileBlobStore.getBlobBuilder(null);
assertNotNull(builder);
verify(mockManager).getCurrentMessageVolume();
verify(defaultVolume).getIncomingDirectory();
verify(dir).getNewIncomingFile();
}

@Test(expected = ServiceException.class)
public void testGetBlobBuilderWhenIncomingDirectoryNull() throws Exception {
when(volume.getIncomingDirectory()).thenReturn(null);
when(volume.getName()).thenReturn("testVolume");
fileBlobStore.getBlobBuilder(volume);
}
}

19 changes: 19 additions & 0 deletions store/src/java-test/com/zimbra/cs/store/MockStoreManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,21 @@ public BlobBuilder getBlobBuilder() {
return new MockBlobBuilder();
}

@Override
public BlobBuilder getBlobBuilder(Volume volume) throws IOException, ServiceException {
return new MockBlobBuilder();
}

@Override
public Blob storeIncoming(InputStream data, boolean storeAsIs) throws IOException {
return new MockBlob(ByteStreams.toByteArray(data));
}

@Override
public Blob storeIncoming(InputStream data, boolean storeAsIs, Volume volume) throws IOException, ServiceException {
return new MockBlob(ByteStreams.toByteArray(data));
}

@Override
public StagedBlob stage(InputStream data, long actualSize, Mailbox mbox) throws IOException {
return new MockStagedBlob(mbox, ByteStreams.toByteArray(data));
Expand Down Expand Up @@ -142,6 +152,15 @@ public MailboxBlob renameTo(StagedBlob src, Mailbox destMbox, int destItemId, in
return blob;
}

@Override
public MailboxBlob renameTo(StagedBlob src, Mailbox destMbox, int destMsgId, int destRevision, Volume volume)
throws IOException, ServiceException {
MockMailboxBlob blob = new MockMailboxBlob(destMbox, destMsgId, destRevision,
src.getLocator(), ((MockStagedBlob) src).content);
blobs.put(blobKey(destMbox, destMsgId, destRevision), blob);
return blob;
}

@Override
public boolean delete(Blob blob) throws IOException {
if (blob instanceof MockLocalBlob) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,21 @@ public BlobBuilder getBlobBuilder() throws IOException, ServiceException {
return null;
}

@Override
public BlobBuilder getBlobBuilder(Volume volume) throws IOException, ServiceException {
return null;
}

@Override
public Blob storeIncoming(InputStream data, boolean storeAsIs) throws IOException, ServiceException {
return null;
}

@Override
public Blob storeIncoming(InputStream data, boolean storeAsIs, Volume volume) throws IOException, ServiceException {
return null;
}

@Override
public StagedBlob stage(InputStream data, long actualSize, Mailbox mbox) throws IOException, ServiceException {
return null;
Expand Down Expand Up @@ -85,6 +95,12 @@ public MailboxBlob renameTo(StagedBlob src, Mailbox destMbox, int destMsgId, int
return null;
}

@Override
public MailboxBlob renameTo(StagedBlob src, Mailbox destMbox, int destMsgId,
int destRevision, Volume volume) throws IOException, ServiceException {
return null;
}

@Override
public boolean delete(Blob blob) throws IOException {
return false;
Expand Down
53 changes: 53 additions & 0 deletions store/src/java/com/zimbra/cs/store/StoreManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,18 @@ public enum StoreFeature {
*/
public abstract BlobBuilder getBlobBuilder() throws IOException, ServiceException;

/**
* Returns a 'BlobBuilder' which can be used to store a blob in incoming
* directory asynchronously one chunk at a time. Blob will be compressed
* if volume supports compression and blob size is over the compression
* threshold.
* @param volume
* @return the BlobBuilder to use to construct the Blob
* @throws IOException if an I/O error occurred
* @throws ServiceException if a service exception occurred
*/
public abstract BlobBuilder getBlobBuilder(Volume volume) throws IOException, ServiceException;

/**
* Store a blob in incoming directory. Blob will be compressed if volume supports compression
* and blob size is over the compression threshold.
Expand All @@ -277,6 +289,20 @@ public Blob storeIncoming(InputStream data)
return storeIncoming(data, false);
}

/**
* Store a blob in incoming directory. Blob will be compressed if volume supports compression
* and blob size is over the compression threshold.
* @param data
* @param vol
* @return
* @throws IOException

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [checkstyle] <com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocParagraphCheck> reported by reviewdog 🐶
Javadoc comment at column 0 has parse error. Details: no viable alternative at input ' *' while parsing JAVADOC_TAG

* @throws ServiceException
*/
public Blob storeIncoming(InputStream data, Volume vol)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [checkstyle] <com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocMethodCheck> reported by reviewdog 🐶
@return tag should be present and have description.

throws IOException, ServiceException {
return storeIncoming(data, false, vol);
}

/**
* Store a blob in incoming directory.
* @param data
Expand All @@ -289,6 +315,18 @@ public Blob storeIncoming(InputStream data)
public abstract Blob storeIncoming(InputStream data, boolean storeAsIs)
throws IOException, ServiceException;

/**
* Store a blob in incoming directory.
* @param data
* @param storeAsIs if true, store the blob as is even if volume supports compression
* @param volume
* @return
* @throws IOException

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [checkstyle] <com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocParagraphCheck> reported by reviewdog 🐶
Javadoc comment at column 0 has parse error. Details: no viable alternative at input ' *' while parsing JAVADOC_TAG

* @throws ServiceException
*/
public abstract Blob storeIncoming(InputStream data, boolean storeAsIs, Volume volume)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [checkstyle] <com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocMethodCheck> reported by reviewdog 🐶
@return tag should be present and have description.

throws IOException, ServiceException;

/**
* Stage an incoming <code>InputStream</code> to an
* appropriate place for subsequent storage in a <code>Mailbox</code> via
Expand Down Expand Up @@ -402,6 +440,21 @@ public abstract MailboxBlob link(StagedBlob src, Mailbox destMbox, int destMsgId
public abstract MailboxBlob renameTo(StagedBlob src, Mailbox destMbox, int destMsgId, int destRevision)
throws IOException, ServiceException;

/**
* Rename a blob to a blob in mailbox directory.
* This effectively makes the StagedBlob permanent, implementations may not need to do anything if the stage operation creates permanent items

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [checkstyle] <com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck> reported by reviewdog 🐶
Line exceeds 120 characters in comments

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [checkstyle] <com.puppycrawl.tools.checkstyle.checks.sizes.LineLengthCheck> reported by reviewdog 🐶
Line is longer than 120 characters (found 146).

* @param src
* @param destMbox
* @param destMsgId mail_item.id value for message in destMbox
* @param destRevision mail_item.mod_content value for message in destMbox
* @param volume volume info
* @return MailboxBlob object representing the renamed blob
* @throws IOException
* @throws ServiceException
*/
public abstract MailboxBlob renameTo(StagedBlob src, Mailbox destMbox, int destMsgId, int destRevision,
Volume volume) throws IOException, ServiceException;

/**
* Deletes a blob from incoming directory. If blob doesn't exist, no exception is
* thrown and false is returned.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ public BlobBuilder getBlobBuilder() throws IOException, ServiceException {
return new ExternalBlobBuilder(new ExternalBlob(incoming.getNewIncomingFile()));
}

@Override
public BlobBuilder getBlobBuilder(Volume volume) throws IOException, ServiceException {
throw new UnsupportedOperationException("method not supported");
}

@Override
public InputStream getContent(MailboxBlob mblob) throws IOException {
if (mblob == null) {
Expand Down Expand Up @@ -244,6 +249,12 @@ public MailboxBlob renameTo(StagedBlob src, Mailbox destMbox, int destMsgId, int
return mblob.setSize(staged.getSize()).setDigest(staged.getDigest());
}

@Override
public MailboxBlob renameTo(StagedBlob src, Mailbox destMbox, int destMsgId, int destRevision, Volume volume) throws IOException,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [checkstyle] <com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck> reported by reviewdog 🐶
Line exceeds 120 characters in comments

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [checkstyle] <com.puppycrawl.tools.checkstyle.checks.sizes.LineLengthCheck> reported by reviewdog 🐶
Line is longer than 120 characters (found 133).

ServiceException {
throw new UnsupportedOperationException("method not supported");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [checkstyle] <com.puppycrawl.tools.checkstyle.checks.indentation.IndentationCheck> reported by reviewdog 🐶
'method def' child has incorrect indentation level 7, expected level should be 8.

}

@Override
public void shutdown() {
IncomingDirectory.stopSweeper();
Expand Down Expand Up @@ -369,6 +380,12 @@ public Blob storeIncoming(InputStream data, boolean storeAsIs) throws IOExceptio
return builder.init().append(data).finish();
}

@Override
public Blob storeIncoming(InputStream data, boolean storeAsIs, Volume volume) throws IOException,
ServiceException {
throw new UnsupportedOperationException("method not supported");
}

/**
* Get a set of all blobs which exist in the store associated with a mailbox
* Optional operation used to find orphaned blobs
Expand Down
49 changes: 37 additions & 12 deletions store/src/java/com/zimbra/cs/store/file/FileBlobStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.Optional;

import com.zimbra.common.localconfig.DebugConfig;
import com.zimbra.common.localconfig.LC;
Expand Down Expand Up @@ -84,7 +86,12 @@ public boolean supports(StoreFeature feature) {
}

private Blob getUniqueIncomingBlob() throws IOException, ServiceException {
Volume volume = MANAGER.getCurrentMessageVolume();
return getUniqueIncomingBlob(null);
}

private Blob getUniqueIncomingBlob(Volume volume) throws IOException, ServiceException {
volume = Optional.ofNullable(volume)
.orElseGet(MANAGER::getCurrentMessageVolume);
IncomingDirectory incdir = volume.getIncomingDirectory();
if (incdir == null) {
throw ServiceException.FAILURE("storing blob to volume without incoming directory: " + volume.getName(), null);
Expand All @@ -96,32 +103,42 @@ private Blob getUniqueIncomingBlob() throws IOException, ServiceException {

@Override
public BlobBuilder getBlobBuilder() throws IOException, ServiceException {
Blob blob = getUniqueIncomingBlob();
return getBlobBuilder(null);
}

@Override
public BlobBuilder getBlobBuilder(Volume volume) throws IOException, ServiceException {
Blob blob = (Objects.isNull(volume)) ? getUniqueIncomingBlob() : getUniqueIncomingBlob(volume);
return new VolumeBlobBuilder(blob);
}

@Override
public Blob storeIncoming(InputStream in, boolean storeAsIs)
throws IOException, ServiceException {
BlobBuilder builder = getBlobBuilder();
return storeIncoming(in, storeAsIs, null);
}

@Override
public Blob storeIncoming(InputStream in, boolean storeAsIs, Volume volume)
throws IOException, ServiceException {
BlobBuilder builder = (Objects.isNull(volume)) ? getBlobBuilder() : getBlobBuilder(volume);
// if the blob is already compressed, *don't* calculate a digest/size from what we write
builder.disableCompression(storeAsIs).disableDigest(storeAsIs);

return builder.init().append(in).finish();
}

@Override
public VolumeStagedBlob stage(InputStream in, long actualSize, Mailbox mbox)
throws IOException, ServiceException {
// mailbox store is on the same volume as incoming directory, so just storeIncoming() and wrap it
Blob blob = storeIncoming(in);
return new VolumeStagedBlob(mbox, (VolumeBlob) blob).markStagedDirectly();
return new VolumeStagedBlob(mbox, (VolumeBlob) storeIncoming(in))
.markStagedDirectly();
}

@Override
public StagedBlob stage(InputStream data, long actualSize, Mailbox mbox, Volume volume) throws IOException, ServiceException {
// mailbox store is on the same volume as incoming directory, so no need to stage the blob
throw ServiceException.FAILURE("Operation can not be completed because the required StoreManager is not available", null);
return new VolumeStagedBlob(mbox, (VolumeBlob) storeIncoming(data, volume))
.markStagedDirectly();
}

@Override
Expand Down Expand Up @@ -284,7 +301,14 @@ In order to support multiple StoreManagers, sometime may need to copy ExternalBl
@Override
public VolumeMailboxBlob renameTo(StagedBlob src, Mailbox destMbox, int destItemId, int destRevision)
throws IOException, ServiceException {
Volume volume = MANAGER.getCurrentMessageVolume();
return renameTo(src, destMbox, destItemId, destRevision, null);
}

@Override
public VolumeMailboxBlob renameTo(StagedBlob src, Mailbox destMbox, int destItemId, int destRevision, Volume volume)
throws IOException, ServiceException {
volume = Optional.ofNullable(volume)
.orElseGet(MANAGER::getCurrentMessageVolume);
VolumeBlob blob = ((VolumeStagedBlob) src).getLocalBlob();
File srcFile = blob.getFile();
String srcPath = srcFile.getAbsolutePath();
Expand All @@ -301,22 +325,23 @@ public VolumeMailboxBlob renameTo(StagedBlob src, Mailbox destMbox, int destItem
long srcSize = srcFile.length();
long srcRawSize = blob.getRawSize();
ZimbraLog.store.debug("Renaming %s (size=%d, raw size=%d) to %s for mailbox %d, id %d.",
srcPath, srcSize, srcRawSize, destPath, destMbox.getId(), destItemId);
srcPath, srcSize, srcRawSize, destPath, destMbox.getId(), destItemId);
}

short srcVolumeId = blob.getVolumeId();
if (srcVolumeId == volume.getId()) {
boolean renamed = srcFile.renameTo(destFile);
if (SystemUtil.ON_WINDOWS) {
// On Windows renameTo fails if the dest already exists. So delete
// on Windows renameTo fails if the dest already exists. So delete
// the destination and try the rename again
if (!renamed && destFile.exists()) {
destFile.delete();
renamed = srcFile.renameTo(destFile);
}
}
if (!renamed)
if (!renamed) {
throw new IOException("Unable to rename " + srcPath + " to " + destPath);
}
} else {
// Can't rename across volumes. Copy then delete instead.
FileUtil.copy(srcFile, destFile, !DebugConfig.disableMessageStoreFsync);
Expand Down
Loading