diff --git a/src/main/java/com/ycwl/basic/storage/adapters/AliOssAdapter.java b/src/main/java/com/ycwl/basic/storage/adapters/AliOssAdapter.java index 4b6c170..e6c003a 100644 --- a/src/main/java/com/ycwl/basic/storage/adapters/AliOssAdapter.java +++ b/src/main/java/com/ycwl/basic/storage/adapters/AliOssAdapter.java @@ -33,25 +33,37 @@ import java.util.Map; import java.util.stream.Collectors; final public class AliOssAdapter extends AStorageAdapter { - private AliOssStorageConfig config; + private volatile AliOssStorageConfig config; + private volatile OSS ossClient; + private final Object clientLock = new Object(); + private final Object configLock = new Object(); @Override public String identity() { - return config.identity(); + AliOssStorageConfig currentConfig = this.config; + if (currentConfig == null) { + return "ALI_OSS_UNCONFIGURED"; + } + return currentConfig.identity(); } @Override public void loadConfig(Map _config) { - AliOssStorageConfig config = new AliOssStorageConfig(); - config.setAccessKeyId(_config.get("accessKeyId")); - config.setAccessKeySecret(_config.get("accessKeySecret")); - config.setBucketName(_config.get("bucketName")); - config.setEndpoint(_config.get("endpoint")); - config.setRegion(_config.get("region")); - config.setUrl(_config.get("url")); - config.setPrefix(_config.get("prefix")); - config.checkEverythingOK(); - this.config = config; + AliOssStorageConfig newConfig = new AliOssStorageConfig(); + newConfig.setAccessKeyId(_config.get("accessKeyId")); + newConfig.setAccessKeySecret(_config.get("accessKeySecret")); + newConfig.setBucketName(_config.get("bucketName")); + newConfig.setEndpoint(_config.get("endpoint")); + newConfig.setRegion(_config.get("region")); + newConfig.setUrl(_config.get("url")); + newConfig.setPrefix(_config.get("prefix")); + newConfig.checkEverythingOK(); + + synchronized (configLock) { + this.config = newConfig; + // 配置更新后,需要重置客户端连接 + resetClient(); + } } @Override @@ -59,11 +71,15 @@ final public class AliOssAdapter extends AStorageAdapter { if (config == null) { throw new StorageConfigException("配置为空"); } - if (config instanceof AliOssStorageConfig) { - this.config = (AliOssStorageConfig) config; - } else { + if (!(config instanceof AliOssStorageConfig)) { throw new StorageConfigException("配置类型错误,传入的类为:" + config.getClass().getName()); } + + synchronized (configLock) { + this.config = (AliOssStorageConfig) config; + // 配置更新后,需要重置客户端连接 + resetClient(); + } } @Override @@ -75,12 +91,25 @@ final public class AliOssAdapter extends AStorageAdapter { try (OSSWrapper wrapper = getOssClient()) { OSS ossClient = wrapper.getOSSClient(); ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentLength(inputStream.available()); + + // 读取完整内容并计算实际长度 + byte[] bytes = inputStream.readAllBytes(); + metadata.setContentLength(bytes.length); + if (StringUtils.isNotBlank(contentType)) { metadata.setContentType(contentType); } - PutObjectRequest putObjectRequest = new PutObjectRequest(config.getBucketName(), fullPath, inputStream); - ossClient.putObject(putObjectRequest); + + // 使用字节数组创建新的 InputStream + try (InputStream byteInputStream = new java.io.ByteArrayInputStream(bytes)) { + AliOssStorageConfig currentConfig = this.config; + if (currentConfig == null) { + throw new StorageConfigException("存储适配器未配置"); + } + PutObjectRequest putObjectRequest = new PutObjectRequest(currentConfig.getBucketName(), fullPath, byteInputStream, metadata); + ossClient.putObject(putObjectRequest); + } + return getUrl(path); } catch (Exception e) { throw new UploadFileFailedException("上传文件失败:" + e.getMessage()); @@ -92,7 +121,11 @@ final public class AliOssAdapter extends AStorageAdapter { public boolean deleteFile(String... path) { try (OSSWrapper wrapper = getOssClient()) { OSS ossClient = wrapper.getOSSClient(); - ossClient.deleteObject(config.getBucketName(), buildPath(path)); + AliOssStorageConfig currentConfig = this.config; + if (currentConfig == null) { + return false; + } + ossClient.deleteObject(currentConfig.getBucketName(), buildPath(path)); return true; } catch (ClientException e) { return false; @@ -101,14 +134,22 @@ final public class AliOssAdapter extends AStorageAdapter { @Override public String getUrl(String... path) { - return config.getUrl() + "/" + buildPath(path); + AliOssStorageConfig currentConfig = this.config; + if (currentConfig == null) { + throw new StorageConfigException("存储适配器未配置"); + } + return currentConfig.getUrl() + "/" + buildPath(path); } @Override public String getUrlForDownload(Date expireDate, String... path) { try (OSSWrapper wrapper = getOssClient()) { OSS ossClient = wrapper.getOSSClient(); - URL url = ossClient.generatePresignedUrl(config.getBucketName(), buildPath(path), expireDate, HttpMethod.GET); + AliOssStorageConfig currentConfig = this.config; + if (currentConfig == null) { + throw new StorageConfigException("存储适配器未配置"); + } + URL url = ossClient.generatePresignedUrl(currentConfig.getBucketName(), buildPath(path), expireDate, HttpMethod.GET); return url.toString(); } } @@ -117,7 +158,11 @@ final public class AliOssAdapter extends AStorageAdapter { public String getUrlForUpload(Date expireDate, String contentType, String... path) { try (OSSWrapper wrapper = getOssClient()) { OSS ossClient = wrapper.getOSSClient(); - GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(config.getBucketName(), buildPath(path), HttpMethod.PUT); + AliOssStorageConfig currentConfig = this.config; + if (currentConfig == null) { + throw new StorageConfigException("存储适配器未配置"); + } + GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(currentConfig.getBucketName(), buildPath(path), HttpMethod.PUT); if (StringUtils.isNotBlank(contentType)) { request.setContentType(contentType); } @@ -129,7 +174,11 @@ final public class AliOssAdapter extends AStorageAdapter { @Override public List listDir(String... path) { - ListObjectsV2Request listObjectsV2Request = new ListObjectsV2Request(config.getBucketName()); + AliOssStorageConfig currentConfig = this.config; + if (currentConfig == null) { + throw new StorageConfigException("存储适配器未配置"); + } + ListObjectsV2Request listObjectsV2Request = new ListObjectsV2Request(currentConfig.getBucketName()); listObjectsV2Request.setPrefix(buildPath(path) + "/"); listObjectsV2Request.setMaxKeys(1000); boolean isTruncated = true; @@ -180,7 +229,11 @@ final public class AliOssAdapter extends AStorageAdapter { } List subList = objectList.subList(idx, idx + batchSize); idx += batchSize; - DeleteObjectsRequest request = new DeleteObjectsRequest(config.getBucketName()); + AliOssStorageConfig currentConfig = this.config; + if (currentConfig == null) { + return false; + } + DeleteObjectsRequest request = new DeleteObjectsRequest(currentConfig.getBucketName()); request.setKeys(subList.stream().map(StorageFileObject::getFullPath).collect(Collectors.toList())); try { ossClient.deleteObjects(request); @@ -208,7 +261,11 @@ final public class AliOssAdapter extends AStorageAdapter { public boolean setAcl(StorageAcl acl, String... path) { try (OSSWrapper wrapper = getOssClient()) { OSS ossClient = wrapper.getOSSClient(); - ossClient.setObjectAcl(config.getBucketName(), buildPath(path), convertAcl(acl)); + AliOssStorageConfig currentConfig = this.config; + if (currentConfig == null) { + return false; + } + ossClient.setObjectAcl(currentConfig.getBucketName(), buildPath(path), convertAcl(acl)); return true; } catch (OSSException e) { return false; @@ -219,32 +276,85 @@ final public class AliOssAdapter extends AStorageAdapter { public boolean isExists(String ...path) { try (OSSWrapper wrapper = getOssClient()) { OSS ossClient = wrapper.getOSSClient(); - return ossClient.doesObjectExist(config.getBucketName(), buildPath(path)); + AliOssStorageConfig currentConfig = this.config; + if (currentConfig == null) { + return false; + } + return ossClient.doesObjectExist(currentConfig.getBucketName(), buildPath(path)); } } private OSSWrapper getOssClient() { - OSS ossClient = new OSSClientBuilder().build(config.getEndpoint(), config.getAccessKeyId(), config.getAccessKeySecret()); - return new OSSWrapper(ossClient); + AliOssStorageConfig currentConfig = this.config; + if (currentConfig == null) { + throw new StorageConfigException("存储适配器未配置"); + } + + if (ossClient == null) { + synchronized (clientLock) { + if (ossClient == null) { + // 在同步块内再次检查配置,确保配置一致性 + currentConfig = this.config; + if (currentConfig == null) { + throw new StorageConfigException("存储适配器未配置"); + } + ossClient = new OSSClientBuilder().build( + currentConfig.getEndpoint(), + currentConfig.getAccessKeyId(), + currentConfig.getAccessKeySecret() + ); + } + } + } + return new OSSWrapper(ossClient, false); // false 表示不自动关闭共享客户端 + } + + // 重置客户端连接(配置更新时调用) + private void resetClient() { + synchronized (clientLock) { + if (ossClient != null) { + ossClient.shutdown(); + ossClient = null; + } + } + } + + // 手动关闭客户端的方法,用于适配器销毁时 + public void shutdown() { + synchronized (clientLock) { + if (ossClient != null) { + ossClient.shutdown(); + ossClient = null; + } + } } private String buildPath(String ...paths) { - if (StringUtils.isNotBlank(config.getPrefix())) { - return StorageUtil.joinPath(config.getPrefix(), paths); + AliOssStorageConfig currentConfig = this.config; + if (currentConfig != null && StringUtils.isNotBlank(currentConfig.getPrefix())) { + return StorageUtil.joinPath(currentConfig.getPrefix(), paths); } else { return StorageUtil.joinPath(paths); } } private String getRelativePath(String path) { - return StorageUtil.getRelativePath(path, config.getPrefix()); + AliOssStorageConfig currentConfig = this.config; + String prefix = currentConfig != null ? currentConfig.getPrefix() : null; + return StorageUtil.getRelativePath(path, prefix); } public static class OSSWrapper implements AutoCloseable { private final OSS ossClient; + private final boolean autoClose; public OSSWrapper(OSS ossClient) { + this(ossClient, true); + } + + public OSSWrapper(OSS ossClient, boolean autoClose) { this.ossClient = ossClient; + this.autoClose = autoClose; } // 提供对原始对象的方法访问 @@ -254,8 +364,10 @@ final public class AliOssAdapter extends AStorageAdapter { @Override public void close() { - // 在此处实现资源关闭逻辑(如调用 OSS 的关闭方法) - ossClient.shutdown(); // 假设 OSS 提供 shutdown 方法关闭资源 + // 只有非共享客户端才自动关闭 + if (autoClose && ossClient != null) { + ossClient.shutdown(); + } } } } diff --git a/src/main/java/com/ycwl/basic/storage/adapters/AwsOssAdapter.java b/src/main/java/com/ycwl/basic/storage/adapters/AwsOssAdapter.java index a359787..101cfe4 100644 --- a/src/main/java/com/ycwl/basic/storage/adapters/AwsOssAdapter.java +++ b/src/main/java/com/ycwl/basic/storage/adapters/AwsOssAdapter.java @@ -26,25 +26,37 @@ import java.util.Map; import java.util.stream.Collectors; public class AwsOssAdapter extends AStorageAdapter { - private AwsOssStorageConfig config; + private volatile AwsOssStorageConfig config; + private volatile AmazonS3Client s3Client; + private final Object clientLock = new Object(); + private final Object configLock = new Object(); @Override public String identity() { - return config.identity(); + AwsOssStorageConfig currentConfig = this.config; + if (currentConfig == null) { + return "AWS_OSS_UNCONFIGURED"; + } + return currentConfig.identity(); } @Override public void loadConfig(Map _config) { - AwsOssStorageConfig config = new AwsOssStorageConfig(); - config.setAccessKeyId(_config.get("accessKeyId")); - config.setAccessKeySecret(_config.get("accessKeySecret")); - config.setBucketName(_config.get("bucketName")); - config.setEndpoint(_config.get("endpoint")); - config.setRegion(_config.get("region")); - config.setUrl(_config.get("url")); - config.setPrefix(_config.get("prefix")); - config.checkEverythingOK(); - this.config = config; + AwsOssStorageConfig newConfig = new AwsOssStorageConfig(); + newConfig.setAccessKeyId(_config.get("accessKeyId")); + newConfig.setAccessKeySecret(_config.get("accessKeySecret")); + newConfig.setBucketName(_config.get("bucketName")); + newConfig.setEndpoint(_config.get("endpoint")); + newConfig.setRegion(_config.get("region")); + newConfig.setUrl(_config.get("url")); + newConfig.setPrefix(_config.get("prefix")); + newConfig.checkEverythingOK(); + + synchronized (configLock) { + this.config = newConfig; + // 配置更新后,需要重置客户端连接 + resetClient(); + } } @Override @@ -52,11 +64,15 @@ public class AwsOssAdapter extends AStorageAdapter { if (config == null) { throw new StorageConfigException("配置为空"); } - if (config instanceof AwsOssStorageConfig) { - this.config = (AwsOssStorageConfig) config; - } else { + if (!(config instanceof AwsOssStorageConfig)) { throw new StorageConfigException("配置类型错误,传入的类为:" + config.getClass().getName()); } + + synchronized (configLock) { + this.config = (AwsOssStorageConfig) config; + // 配置更新后,需要重置客户端连接 + resetClient(); + } } @Override @@ -68,13 +84,22 @@ public class AwsOssAdapter extends AStorageAdapter { try (S3Wrapper wrapper = getS3Client()) { AmazonS3Client s3Client = wrapper.s3Client(); ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentLength(inputStream.available()); + + // 读取完整内容并计算实际长度 + byte[] bytes = inputStream.readAllBytes(); + metadata.setContentLength(bytes.length); + if (StringUtils.isNotBlank(contentType)) { metadata.setContentType(contentType); } - PutObjectRequest putObjectRequest = new PutObjectRequest(config.getBucketName(), fullPath, inputStream, metadata); - putObjectRequest.withCannedAcl(CannedAccessControlList.PublicRead); // 设置访问权限,让所有用户都允许访问 - s3Client.putObject(putObjectRequest); + + // 使用字节数组创建新的 InputStream + try (InputStream byteInputStream = new java.io.ByteArrayInputStream(bytes)) { + PutObjectRequest putObjectRequest = new PutObjectRequest(config.getBucketName(), fullPath, byteInputStream, metadata); + putObjectRequest.withCannedAcl(CannedAccessControlList.PublicRead); // 设置访问权限,让所有用户都允许访问 + s3Client.putObject(putObjectRequest); + } + return getUrl(path); } catch (Exception e) { throw new UploadFileFailedException("上传文件失败:" + e.getMessage()); @@ -150,6 +175,7 @@ public class AwsOssAdapter extends AStorageAdapter { object.setPath(getRelativePath(item.getKey().substring(0, item.getKey().lastIndexOf("/")))); object.setName(item.getKey().substring(item.getKey().lastIndexOf("/") + 1)); object.setSize(item.getSize()); + object.setModifyTime(item.getLastModified()); object.setRawObject(item); return object; }).collect(Collectors.toList()); @@ -220,14 +246,57 @@ public class AwsOssAdapter extends AStorageAdapter { } private S3Wrapper getS3Client() { - BasicAWSCredentials basicAwsCred = new BasicAWSCredentials(config.getAccessKeyId(), config.getAccessKeySecret()); - ClientConfiguration clientConfiguration = new ClientConfiguration(); - clientConfiguration.setProtocol(Protocol.HTTPS); - AmazonS3Client s3 = new AmazonS3Client(basicAwsCred,clientConfiguration); - S3ClientOptions options = S3ClientOptions.builder().setPathStyleAccess(true).setPayloadSigningEnabled(true).disableChunkedEncoding().build(); - s3.setS3ClientOptions(options); - s3.setEndpoint(config.getEndpoint()); - return new S3Wrapper(s3); + AwsOssStorageConfig currentConfig = this.config; + if (currentConfig == null) { + throw new StorageConfigException("存储适配器未配置"); + } + + if (s3Client == null) { + synchronized (clientLock) { + if (s3Client == null) { + // 在同步块内再次检查配置,确保配置一致性 + currentConfig = this.config; + if (currentConfig == null) { + throw new StorageConfigException("存储适配器未配置"); + } + BasicAWSCredentials basicAwsCred = new BasicAWSCredentials( + currentConfig.getAccessKeyId(), + currentConfig.getAccessKeySecret() + ); + ClientConfiguration clientConfiguration = new ClientConfiguration(); + clientConfiguration.setProtocol(Protocol.HTTPS); + s3Client = new AmazonS3Client(basicAwsCred, clientConfiguration); + S3ClientOptions options = S3ClientOptions.builder() + .setPathStyleAccess(true) + .setPayloadSigningEnabled(true) + .disableChunkedEncoding() + .build(); + s3Client.setS3ClientOptions(options); + s3Client.setEndpoint(currentConfig.getEndpoint()); + } + } + } + return new S3Wrapper(s3Client, false); // false 表示不自动关闭共享客户端 + } + + // 重置客户端连接(配置更新时调用) + private void resetClient() { + synchronized (clientLock) { + if (s3Client != null) { + s3Client.shutdown(); + s3Client = null; + } + } + } + + // 手动关闭客户端的方法,用于适配器销毁时 + public void shutdown() { + synchronized (clientLock) { + if (s3Client != null) { + s3Client.shutdown(); + s3Client = null; + } + } } private String buildPath(String... paths) { @@ -242,10 +311,18 @@ public class AwsOssAdapter extends AStorageAdapter { return StorageUtil.getRelativePath(path, config.getPrefix()); } - public record S3Wrapper(AmazonS3Client s3Client) implements AutoCloseable { + public record S3Wrapper(AmazonS3Client s3Client, boolean autoClose) implements AutoCloseable { + + public S3Wrapper(AmazonS3Client s3Client) { + this(s3Client, true); + } + @Override public void close() { - s3Client.shutdown(); + // 只有非共享客户端才自动关闭 + if (autoClose && s3Client != null) { + s3Client.shutdown(); + } } } } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/storage/adapters/LocalStorageAdapter.java b/src/main/java/com/ycwl/basic/storage/adapters/LocalStorageAdapter.java index 49e9883..9a678f6 100644 --- a/src/main/java/com/ycwl/basic/storage/adapters/LocalStorageAdapter.java +++ b/src/main/java/com/ycwl/basic/storage/adapters/LocalStorageAdapter.java @@ -3,71 +3,207 @@ package com.ycwl.basic.storage.adapters; import com.ycwl.basic.storage.entity.StorageConfig; import com.ycwl.basic.storage.entity.StorageFileObject; import com.ycwl.basic.storage.enums.StorageAcl; +import com.ycwl.basic.storage.exceptions.StorageConfigException; +import com.ycwl.basic.storage.exceptions.StorageException; +import com.ycwl.basic.storage.exceptions.UploadFileFailedException; +import com.ycwl.basic.storage.utils.StorageUtil; +import org.apache.commons.lang3.StringUtils; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; import java.io.InputStream; -import java.util.Collections; +import java.nio.file.*; +import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; +import java.util.stream.Stream; + +public class LocalStorageAdapter extends AStorageAdapter { + private volatile String basePath; + private volatile String baseUrl; + private volatile String prefix = ""; + private final Object configLock = new Object(); -public class LocalStorageAdapter extends AStorageAdapter{ @Override public String identity() { - return ""; + return "LOCAL_STORAGE"; } @Override public void loadConfig(Map config) { - + String newBasePath = config.get("basePath"); + String newBaseUrl = config.get("baseUrl"); + String newPrefix = StringUtils.isNotBlank(config.get("prefix")) ? config.get("prefix") : ""; + + if (StringUtils.isBlank(newBasePath)) { + throw new StorageConfigException("本地存储配置错误:basePath 不能为空"); + } + if (StringUtils.isBlank(newBaseUrl)) { + throw new StorageConfigException("本地存储配置错误:baseUrl 不能为空"); + } + + // 确保基础目录存在 + try { + Files.createDirectories(Paths.get(newBasePath)); + } catch (IOException e) { + throw new StorageConfigException("创建本地存储目录失败:" + e.getMessage()); + } + + synchronized (configLock) { + this.basePath = newBasePath; + this.baseUrl = newBaseUrl; + this.prefix = newPrefix; + } } @Override public void setConfig(StorageConfig config) { - + throw new StorageConfigException("LocalStorageAdapter 不支持 StorageConfig 类型配置"); } @Override public String uploadFile(String contentType, InputStream inputStream, String... path) { - return ""; + if (inputStream == null) { + return null; + } + + String fullPath = buildLocalPath(path); + try { + // 确保父目录存在 + Path filePath = Paths.get(fullPath); + Files.createDirectories(filePath.getParent()); + + // 复制文件内容到本地 + Files.copy(inputStream, filePath, StandardCopyOption.REPLACE_EXISTING); + + return getUrl(path); + } catch (IOException e) { + throw new UploadFileFailedException("本地文件保存失败:" + e.getMessage()); + } } @Override public boolean deleteFile(String... path) { - return false; + try { + String fullPath = buildLocalPath(path); + return Files.deleteIfExists(Paths.get(fullPath)); + } catch (IOException e) { + return false; + } } @Override public String getUrl(String... path) { - return ""; + String relativePath = buildRelativePath(path); + return baseUrl + "/" + relativePath; } @Override public String getUrlForDownload(Date expireDate, String... path) { - return ""; + // 本地存储不支持带过期时间的URL,直接返回普通URL + return getUrl(path); } @Override public String getUrlForUpload(Date expireDate, String contentType, String... path) { - return ""; + // 本地存储不支持预签名上传URL + return getUrl(path); } @Override public List listDir(String... path) { - return Collections.emptyList(); + String fullPath = buildLocalPath(path); + Path dirPath = Paths.get(fullPath); + + if (!Files.exists(dirPath) || !Files.isDirectory(dirPath)) { + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + try (Stream paths = Files.list(dirPath)) { + paths.forEach(filePath -> { + try { + StorageFileObject obj = new StorageFileObject(); + obj.setName(filePath.getFileName().toString()); + obj.setPath(getRelativePath(filePath.getParent().toString())); + obj.setSize(Files.size(filePath)); + obj.setModifyTime(new Date(Files.getLastModifiedTime(filePath).toMillis())); + obj.setRawObject(filePath.toFile()); + result.add(obj); + } catch (IOException e) { + // 忽略无法读取的文件 + } + }); + } catch (IOException e) { + throw new StorageException("列举本地文件失败:" + e.getMessage()); + } + + return result; } @Override public boolean deleteDir(String... path) { - return false; + String fullPath = buildLocalPath(path); + Path dirPath = Paths.get(fullPath); + + if (!Files.exists(dirPath)) { + return true; + } + + try { + // 递归删除目录及其内容 + try (Stream paths = Files.walk(dirPath)) { + paths.sorted((a, b) -> b.compareTo(a)) // 从深到浅排序,确保先删除文件再删除目录 + .forEach(filePath -> { + try { + Files.deleteIfExists(filePath); + } catch (IOException e) { + // 忽略删除失败的文件 + } + }); + } + return !Files.exists(dirPath); + } catch (IOException e) { + return false; + } } @Override public boolean setAcl(StorageAcl acl, String... path) { - return false; + // 本地存储不支持ACL设置 + return true; } @Override public boolean isExists(String... path) { - return false; + String fullPath = buildLocalPath(path); + return Files.exists(Paths.get(fullPath)); + } + + private String buildLocalPath(String... paths) { + String relativePath = buildRelativePath(paths); + return Paths.get(basePath, relativePath).toString(); + } + + private String buildRelativePath(String... paths) { + if (StringUtils.isNotBlank(prefix)) { + return StorageUtil.joinPath(prefix, paths); + } else { + return StorageUtil.joinPath(paths); + } + } + + private String getRelativePath(String fullPath) { + String basePathNormalized = Paths.get(basePath).toString(); + if (fullPath.startsWith(basePathNormalized)) { + String relative = fullPath.substring(basePathNormalized.length()); + if (relative.startsWith(File.separator)) { + relative = relative.substring(1); + } + return StorageUtil.getRelativePath(relative, prefix); + } + return fullPath; } }