Repository: zfile-dev/zfile Branch: main Commit: 3fe5f5b0b97d Files: 565 Total size: 1.1 MB Directory structure: gitextract_wuofd07t/ ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── bug_report.yml │ └── config.yml ├── .gitignore ├── .package/ │ └── script/ │ ├── log.sh │ ├── restart.sh │ ├── start.sh │ ├── status.sh │ ├── stop.sh │ └── 双击我启动.bat ├── Dockerfile ├── LICENSE ├── README.md ├── pom.xml └── src/ └── main/ ├── java/ │ └── im/ │ └── zhaojun/ │ └── zfile/ │ ├── ZfileApplication.java │ ├── core/ │ │ ├── annotation/ │ │ │ ├── ApiLimit.java │ │ │ └── DemoDisable.java │ │ ├── aspect/ │ │ │ ├── ApiLimitAspect.java │ │ │ ├── CommonResultControllerAdvice.java │ │ │ └── DemoDisableAspect.java │ │ ├── cache/ │ │ │ └── ZFileCacheManager.java │ │ ├── config/ │ │ │ ├── ZFileProperties.java │ │ │ ├── datasource/ │ │ │ │ └── DataSourceBeanPostProcessor.java │ │ │ ├── docs/ │ │ │ │ └── Knife4jConfiguration.java │ │ │ ├── jackson/ │ │ │ │ ├── JSONStringDeserializer.java │ │ │ │ └── JSONStringSerializer.java │ │ │ ├── mybatis/ │ │ │ │ ├── CollectionIntegerTypeHandler.java │ │ │ │ ├── CollectionStrTypeHandler.java │ │ │ │ ├── CollectionTypeHandler.java │ │ │ │ ├── MyBatisPlusConfig.java │ │ │ │ ├── MyDatabaseIdProvider.java │ │ │ │ └── MyMetaObjectHandler.java │ │ │ ├── security/ │ │ │ │ ├── SaSessionForJacksonCustomized.java │ │ │ │ ├── SaTokenConfigure.java │ │ │ │ ├── SaTokenDaoRedisJackson.java │ │ │ │ └── StpInterfaceImpl.java │ │ │ ├── spring/ │ │ │ │ ├── JacksonEnumDeserializer.java │ │ │ │ ├── SpringCacheConfig.java │ │ │ │ ├── StringToEnumConverterFactory.java │ │ │ │ └── WebMvcConfig.java │ │ │ └── totp/ │ │ │ ├── TotpAutoConfiguration.java │ │ │ └── TotpProperties.java │ │ ├── constant/ │ │ │ ├── MdcConstant.java │ │ │ ├── RuleTypeConstant.java │ │ │ ├── ZFileConstant.java │ │ │ └── ZFileHttpHeaderConstant.java │ │ ├── controller/ │ │ │ ├── FrontIndexController.java │ │ │ └── LogController.java │ │ ├── exception/ │ │ │ ├── ErrorCode.java │ │ │ ├── GlobalExceptionHandler.java │ │ │ ├── biz/ │ │ │ │ ├── APIHttpRequestBizException.java │ │ │ │ ├── CorsBizException.java │ │ │ │ ├── FilePathSecurityBizException.java │ │ │ │ ├── GetPreviewTextContentBizException.java │ │ │ │ ├── InitializeStorageSourceBizException.java │ │ │ │ ├── InvalidStorageSourceBizException.java │ │ │ │ ├── StorageSourceFileForbiddenAccessBizException.java │ │ │ │ └── StorageSourceIllegalOperationBizException.java │ │ │ ├── core/ │ │ │ │ ├── BizException.java │ │ │ │ ├── ErrorPageBizException.java │ │ │ │ └── SystemException.java │ │ │ ├── status/ │ │ │ │ ├── BadRequestAccessException.java │ │ │ │ ├── ForbiddenAccessException.java │ │ │ │ ├── MethodNotAllowedAccessException.java │ │ │ │ ├── NotFoundAccessException.java │ │ │ │ └── UnauthorizedAccessException.java │ │ │ └── system/ │ │ │ ├── UploadFileFailSystemException.java │ │ │ └── ZFileAuthorizationSystemException.java │ │ ├── filter/ │ │ │ ├── CorsFilter.java │ │ │ ├── MDCFilter.java │ │ │ └── SecurityFilter.java │ │ ├── io/ │ │ │ └── EnsureContentLengthInputStreamResource.java │ │ ├── model/ │ │ │ └── request/ │ │ │ └── PageQueryRequest.java │ │ ├── util/ │ │ │ ├── AjaxJson.java │ │ │ ├── ArrayUtils.java │ │ │ ├── CharPool.java │ │ │ ├── CharSequenceUtil.java │ │ │ ├── ClassUtils.java │ │ │ ├── CollectionUtils.java │ │ │ ├── DnsUtil.java │ │ │ ├── EnumConvertUtils.java │ │ │ ├── FileComparator.java │ │ │ ├── FileResponseUtil.java │ │ │ ├── FileSizeConverter.java │ │ │ ├── FileUtils.java │ │ │ ├── HttpUtil.java │ │ │ ├── NaturalOrderComparator.java │ │ │ ├── NumberUtils.java │ │ │ ├── OnlyOfficeKeyCacheUtils.java │ │ │ ├── PatternMatcherUtils.java │ │ │ ├── PlaceholderUtils.java │ │ │ ├── ProxyDownloadUrlUtils.java │ │ │ ├── RequestHolder.java │ │ │ ├── RequestUtils.java │ │ │ ├── SizeToStrUtils.java │ │ │ ├── SpringMvcUtils.java │ │ │ ├── StrPool.java │ │ │ ├── StringUtils.java │ │ │ ├── UrlUtils.java │ │ │ ├── ZFileAuthUtil.java │ │ │ └── matcher/ │ │ │ ├── AbstractRuleMatcher.java │ │ │ ├── IRuleMatcher.java │ │ │ ├── RuleMatcherFactory.java │ │ │ └── impl/ │ │ │ ├── AntPathRuleMatcher.java │ │ │ ├── IpRuleMatcher.java │ │ │ ├── RegexRuleMatcher.java │ │ │ └── SpringSimpleRuleMatcher.java │ │ └── validation/ │ │ ├── StringListValue.java │ │ └── StringListValueConstraintValidator.java │ └── module/ │ ├── admin/ │ │ ├── controller/ │ │ │ ├── IpHelperController.java │ │ │ └── RuleMatcherTestController.java │ │ └── model/ │ │ └── request/ │ │ └── TestRuleMatcherRequest.java │ ├── config/ │ │ ├── annotation/ │ │ │ └── JSONStringParse.java │ │ ├── constant/ │ │ │ └── SystemConfigConstant.java │ │ ├── controller/ │ │ │ ├── SettingController.java │ │ │ └── SiteController.java │ │ ├── event/ │ │ │ ├── DirectLinkPrefixModifyHandler.java │ │ │ ├── ISystemConfigModifyHandler.java │ │ │ ├── SecureLoginEntryModifyHandler.java │ │ │ └── SystemConfigModifyHandlerChain.java │ │ ├── mapper/ │ │ │ └── SystemConfigMapper.java │ │ ├── model/ │ │ │ ├── dto/ │ │ │ │ ├── LinkExpireDTO.java │ │ │ │ └── SystemConfigDTO.java │ │ │ ├── entity/ │ │ │ │ └── SystemConfig.java │ │ │ ├── enums/ │ │ │ │ └── FileClickModeEnum.java │ │ │ ├── request/ │ │ │ │ ├── UpdateAccessSettingRequest.java │ │ │ │ ├── UpdateLinkSettingRequest.java │ │ │ │ ├── UpdateSecuritySettingRequest.java │ │ │ │ ├── UpdateSiteSettingRequest.java │ │ │ │ ├── UpdateUserNameAndPasswordRequest.java │ │ │ │ └── UpdateViewSettingRequest.java │ │ │ └── result/ │ │ │ └── FrontSiteConfigResult.java │ │ └── service/ │ │ └── SystemConfigService.java │ ├── filter/ │ │ ├── controller/ │ │ │ └── StorageSourceFilterController.java │ │ ├── mapper/ │ │ │ └── FilterConfigMapper.java │ │ ├── model/ │ │ │ ├── entity/ │ │ │ │ └── FilterConfig.java │ │ │ └── enums/ │ │ │ └── FilterConfigHiddenModeEnum.java │ │ └── service/ │ │ └── FilterConfigService.java │ ├── install/ │ │ ├── controller/ │ │ │ └── InstallController.java │ │ ├── model/ │ │ │ └── request/ │ │ │ └── InstallSystemRequest.java │ │ └── service/ │ │ └── InstallService.java │ ├── link/ │ │ ├── aspect/ │ │ │ ├── LinkRateLimiterAspect.java │ │ │ └── RefererCheckAspect.java │ │ ├── cache/ │ │ │ └── LinkRateLimiterCache.java │ │ ├── controller/ │ │ │ ├── DirectLinkController.java │ │ │ ├── ShortLinkController.java │ │ │ └── ShortLinkManagerController.java │ │ ├── convert/ │ │ │ └── ShortLinkConvert.java │ │ ├── dto/ │ │ │ └── DynamicRegisterMappingHandlerDTO.java │ │ ├── event/ │ │ │ └── DeleteExpireLinkEvent.java │ │ ├── mapper/ │ │ │ └── ShortLinkMapper.java │ │ ├── model/ │ │ │ ├── dto/ │ │ │ │ └── CacheInfo.java │ │ │ ├── entity/ │ │ │ │ └── ShortLink.java │ │ │ ├── enums/ │ │ │ │ └── RefererTypeEnum.java │ │ │ ├── request/ │ │ │ │ ├── BatchDeleteRequest.java │ │ │ │ ├── BatchGenerateLinkRequest.java │ │ │ │ ├── QueryDownloadLogRequest.java │ │ │ │ ├── QueryLoginLogRequest.java │ │ │ │ ├── QueryShortLinkLogRequest.java │ │ │ │ ├── ShortLinkResult.java │ │ │ │ └── ShortLinkSearchRequest.java │ │ │ └── result/ │ │ │ └── BatchGenerateLinkResponse.java │ │ └── service/ │ │ ├── DynamicDirectLinkPrefixService.java │ │ ├── LinkDownloadService.java │ │ └── ShortLinkService.java │ ├── log/ │ │ ├── controller/ │ │ │ ├── DownloadLogManagerController.java │ │ │ └── LoginLogController.java │ │ ├── convert/ │ │ │ └── DownloadLogConvert.java │ │ ├── mapper/ │ │ │ ├── DownloadLogMapper.java │ │ │ └── LoginLogMapper.java │ │ ├── model/ │ │ │ ├── entity/ │ │ │ │ ├── DownloadLog.java │ │ │ │ └── LoginLog.java │ │ │ └── result/ │ │ │ └── DownloadLogResult.java │ │ └── service/ │ │ ├── DownloadLogService.java │ │ └── LoginLogService.java │ ├── onlyoffice/ │ │ ├── controller/ │ │ │ └── OnlyOfficeController.java │ │ └── model/ │ │ ├── OnlyOfficeCallback.java │ │ └── OnlyOfficeFile.java │ ├── password/ │ │ ├── controller/ │ │ │ └── StorageSourcePasswordController.java │ │ ├── mapper/ │ │ │ └── PasswordConfigMapper.java │ │ ├── model/ │ │ │ ├── dto/ │ │ │ │ └── VerifyResultDTO.java │ │ │ └── entity/ │ │ │ └── PasswordConfig.java │ │ └── service/ │ │ └── PasswordConfigService.java │ ├── permission/ │ │ ├── controller/ │ │ │ ├── PermissionController.java │ │ │ └── StorageSourcePermissionController.java │ │ ├── convert/ │ │ │ └── PermissionConfigConvert.java │ │ ├── mapper/ │ │ │ └── PermissionConfigMapper.java │ │ ├── model/ │ │ │ ├── entity/ │ │ │ │ └── PermissionConfig.java │ │ │ └── result/ │ │ │ ├── PermissionConfigResult.java │ │ │ └── PermissionInfoResult.java │ │ └── service/ │ │ └── PermissionConfigService.java │ ├── readme/ │ │ ├── controller/ │ │ │ └── StorageSourceReadmeController.java │ │ ├── mapper/ │ │ │ └── ReadmeConfigMapper.java │ │ ├── model/ │ │ │ ├── entity/ │ │ │ │ └── ReadmeConfig.java │ │ │ └── enums/ │ │ │ ├── ReadmeDisplayModeEnum.java │ │ │ └── ReadmePathModeEnum.java │ │ └── service/ │ │ └── ReadmeConfigService.java │ ├── share/ │ │ ├── context/ │ │ │ └── ShareAccessContext.java │ │ ├── controller/ │ │ │ ├── ShareFileManagerController.java │ │ │ └── ShareLinkController.java │ │ ├── mapper/ │ │ │ └── ShareLinkMapper.java │ │ ├── model/ │ │ │ ├── dto/ │ │ │ │ └── ShareEntryDTO.java │ │ │ ├── entity/ │ │ │ │ └── ShareLink.java │ │ │ ├── enums/ │ │ │ │ ├── ShareEntryTypeEnum.java │ │ │ │ └── ShareTypeEnum.java │ │ │ ├── request/ │ │ │ │ ├── CreateShareLinkRequest.java │ │ │ │ ├── ShareFileListRequest.java │ │ │ │ ├── ShareLinkListRequest.java │ │ │ │ └── VerifySharePasswordRequest.java │ │ │ └── result/ │ │ │ ├── CreateShareLinkResult.java │ │ │ ├── ShareFileInfoResult.java │ │ │ └── ShareLinkResult.java │ │ └── service/ │ │ ├── ShareLinkFileService.java │ │ └── ShareLinkService.java │ ├── sso/ │ │ ├── controller/ │ │ │ ├── SsoAPIController.java │ │ │ ├── SsoController.java │ │ │ └── SsoManagerController.java │ │ ├── mapper/ │ │ │ └── SsoConfigMapper.java │ │ ├── model/ │ │ │ ├── entity/ │ │ │ │ └── SsoConfig.java │ │ │ ├── request/ │ │ │ │ └── CheckProviderDuplicateRequest.java │ │ │ └── response/ │ │ │ ├── SsoLoginItemResponse.java │ │ │ └── TokenResponse.java │ │ └── service/ │ │ └── SsoService.java │ ├── storage/ │ │ ├── annotation/ │ │ │ ├── CheckPassword.java │ │ │ ├── CheckPasswords.java │ │ │ ├── LinkRateLimiter.java │ │ │ ├── ProCheck.java │ │ │ ├── RefererCheck.java │ │ │ ├── StorageParamItem.java │ │ │ ├── StorageParamSelect.java │ │ │ ├── StorageParamSelectOption.java │ │ │ ├── StoragePermissionCheck.java │ │ │ └── impl/ │ │ │ └── EncodingStorageParamSelect.java │ │ ├── aspect/ │ │ │ ├── CheckPasswordAspect.java │ │ │ └── FileOperatorCheckAspect.java │ │ ├── chain/ │ │ │ ├── FileChain.java │ │ │ ├── FileContext.java │ │ │ └── command/ │ │ │ ├── FileAccessPermissionVerifyCommand.java │ │ │ ├── FileDownloadPermissionCommand.java │ │ │ ├── FileHiddenCommand.java │ │ │ ├── FileSortCommand.java │ │ │ └── FolderPasswordVerifyCommand.java │ │ ├── constant/ │ │ │ ├── S3SignerTypeConstant.java │ │ │ ├── StorageConfigConstant.java │ │ │ └── StorageSourceConnectionProperties.java │ │ ├── context/ │ │ │ ├── StorageSourceContext.java │ │ │ └── StorageSourceInitializer.java │ │ ├── controller/ │ │ │ ├── base/ │ │ │ │ ├── StorageMetaDataController.java │ │ │ │ └── StorageSourceController.java │ │ │ ├── callback/ │ │ │ │ ├── GoogleDriveCallbackController.java │ │ │ │ └── OneDriveCallbackController.java │ │ │ ├── file/ │ │ │ │ ├── FileController.java │ │ │ │ └── FileOperatorController.java │ │ │ ├── helper/ │ │ │ │ ├── GoogleDriveHelperController.java │ │ │ │ ├── Open115HelperController.java │ │ │ │ ├── Open115UploadUtils.java │ │ │ │ ├── S3HelperController.java │ │ │ │ └── SharePointHelperController.java │ │ │ └── proxy/ │ │ │ ├── Open115UrlController.java │ │ │ ├── ProxyDownloadController.java │ │ │ └── ProxyUploadController.java │ │ ├── convert/ │ │ │ └── StorageSourceConvert.java │ │ ├── enums/ │ │ │ └── StorageParamItemAnnoEnum.java │ │ ├── event/ │ │ │ ├── StorageSourceCopyEvent.java │ │ │ └── StorageSourceDeleteEvent.java │ │ ├── function/ │ │ │ ├── AllowAdminFileOperatorTypeEnumDefaultValueFunc.java │ │ │ ├── AllowAllFileOperatorTypeEnumDefaultValueFunc.java │ │ │ ├── BasicFileOperatorTypeEnumDefaultValueFunc.java │ │ │ ├── DisableAllFileOperatorTypeEnumDefaultValueFunc.java │ │ │ ├── LinkFileOperatorTypeEnumDefaultValueFunc.java │ │ │ ├── SearchFileOperatorTypeEnumDefaultValueFunc.java │ │ │ └── ShortLinkFileOperatorTypeEnumDefaultValueFunc.java │ │ ├── mapper/ │ │ │ ├── StorageSourceConfigMapper.java │ │ │ └── StorageSourceMapper.java │ │ ├── model/ │ │ │ ├── bo/ │ │ │ │ ├── AuthModel.java │ │ │ │ ├── RefreshTokenCacheBO.java │ │ │ │ ├── StorageSourceMetadata.java │ │ │ │ ├── StorageSourceParamDef.java │ │ │ │ └── UploadSignParam.java │ │ │ ├── dto/ │ │ │ │ ├── FileOperatorTypeDefaultValueDTO.java │ │ │ │ ├── OAuth2TokenDTO.java │ │ │ │ ├── RefreshTokenInfoDTO.java │ │ │ │ ├── StorageSourceAllParamDTO.java │ │ │ │ ├── StorageSourceDTO.java │ │ │ │ ├── StorageSourceInitDTO.java │ │ │ │ └── ZFileCORSRule.java │ │ │ ├── entity/ │ │ │ │ ├── StorageSource.java │ │ │ │ └── StorageSourceConfig.java │ │ │ ├── enums/ │ │ │ │ ├── FileOperatorTypeEnum.java │ │ │ │ ├── FileTypeEnum.java │ │ │ │ ├── SearchFolderModeEnum.java │ │ │ │ ├── SearchModeEnum.java │ │ │ │ ├── StorageParamTypeEnum.java │ │ │ │ └── StorageTypeEnum.java │ │ │ ├── param/ │ │ │ │ ├── AliyunParam.java │ │ │ │ ├── DogeCloudParam.java │ │ │ │ ├── FtpParam.java │ │ │ │ ├── GoogleDriveParam.java │ │ │ │ ├── HuaweiParam.java │ │ │ │ ├── IStorageParam.java │ │ │ │ ├── LocalParam.java │ │ │ │ ├── MicrosoftDriveParam.java │ │ │ │ ├── MinIOParam.java │ │ │ │ ├── OneDriveChinaParam.java │ │ │ │ ├── OneDriveParam.java │ │ │ │ ├── Open115Param.java │ │ │ │ ├── OptionalProxyTransferParam.java │ │ │ │ ├── ProxyTransferParam.java │ │ │ │ ├── QiniuParam.java │ │ │ │ ├── S3BaseParam.java │ │ │ │ ├── S3Param.java │ │ │ │ ├── SftpParam.java │ │ │ │ ├── SharePointChinaParam.java │ │ │ │ ├── SharePointParam.java │ │ │ │ ├── TencentParam.java │ │ │ │ ├── UpYunParam.java │ │ │ │ └── WebdavParam.java │ │ │ ├── request/ │ │ │ │ ├── GetGoogleDriveListRequest.java │ │ │ │ ├── GetS3BucketListRequest.java │ │ │ │ ├── GetS3CorsListRequest.java │ │ │ │ ├── SharePointInfoRequest.java │ │ │ │ ├── SharePointSearchSitesRequest.java │ │ │ │ ├── SharePointSiteListsRequest.java │ │ │ │ ├── admin/ │ │ │ │ │ ├── CopyStorageSourceRequest.java │ │ │ │ │ ├── UpdateStorageIdRequest.java │ │ │ │ │ └── UpdateStorageSortRequest.java │ │ │ │ ├── base/ │ │ │ │ │ ├── FileItemRequest.java │ │ │ │ │ ├── FileListConfigRequest.java │ │ │ │ │ ├── FileListRequest.java │ │ │ │ │ ├── SaveStorageSourceRequest.java │ │ │ │ │ └── SearchStorageRequest.java │ │ │ │ └── operator/ │ │ │ │ ├── BatchDeleteRequest.java │ │ │ │ ├── BatchMoveOrCopyFileRequest.java │ │ │ │ ├── NewFolderRequest.java │ │ │ │ ├── RenameFileRequest.java │ │ │ │ ├── RenameFolderRequest.java │ │ │ │ └── UploadFileRequest.java │ │ │ └── result/ │ │ │ ├── FileInfoResult.java │ │ │ ├── FileItemResult.java │ │ │ ├── GoogleDriveInfoResult.java │ │ │ ├── Open115AuthDeviceCodeResult.java │ │ │ ├── Open115GetStatusResult.java │ │ │ ├── S3BucketNameResult.java │ │ │ ├── SharepointSiteListResult.java │ │ │ ├── SharepointSiteResult.java │ │ │ ├── StorageSourceAdminResult.java │ │ │ ├── StorageSourceConfigResult.java │ │ │ ├── StorageSourceResult.java │ │ │ └── operator/ │ │ │ └── BatchOperatorResult.java │ │ ├── oauth2/ │ │ │ └── service/ │ │ │ ├── AbstractMicrosoftOAuth2Service.java │ │ │ ├── GoogleDriveOAuth2ServiceImpl.java │ │ │ ├── IOAuth2Service.java │ │ │ ├── OneDriveChinaOAuth2ServiceImpl.java │ │ │ └── OneDriveOAuth2ServiceImpl.java │ │ ├── service/ │ │ │ ├── StorageSourceConfigService.java │ │ │ ├── StorageSourceService.java │ │ │ ├── base/ │ │ │ │ ├── AbstractBaseFileService.java │ │ │ │ ├── AbstractMicrosoftDriveService.java │ │ │ │ ├── AbstractOneDriveServiceBase.java │ │ │ │ ├── AbstractProxyTransferService.java │ │ │ │ ├── AbstractS3BaseFileService.java │ │ │ │ ├── AbstractSharePointServiceBase.java │ │ │ │ ├── BaseFileService.java │ │ │ │ └── RefreshTokenService.java │ │ │ └── impl/ │ │ │ ├── AliyunServiceImpl.java │ │ │ ├── DogeCloudServiceImpl.java │ │ │ ├── FtpServiceImpl.java │ │ │ ├── GoogleDriveServiceImpl.java │ │ │ ├── HuaweiServiceImpl.java │ │ │ ├── LocalServiceImpl.java │ │ │ ├── MinIOServiceImpl.java │ │ │ ├── OneDriveChinaServiceImpl.java │ │ │ ├── OneDriveServiceImpl.java │ │ │ ├── Open115ServiceImpl.java │ │ │ ├── QiniuServiceImpl.java │ │ │ ├── S3ServiceImpl.java │ │ │ ├── SftpServiceImpl.java │ │ │ ├── SharePointChinaServiceImpl.java │ │ │ ├── SharePointServiceImpl.java │ │ │ ├── TencentServiceImpl.java │ │ │ ├── UpYunServiceImpl.java │ │ │ └── WebdavServiceImpl.java │ │ └── support/ │ │ ├── Open115IdCacheService.java │ │ ├── StorageSourceSupport.java │ │ ├── ftp/ │ │ │ ├── FtpClientFactory.java │ │ │ └── FtpClientPool.java │ │ ├── sftp/ │ │ │ ├── SFtpClientFactory.java │ │ │ └── SFtpClientPool.java │ │ └── webdav/ │ │ └── CustomSardine.java │ └── user/ │ ├── aspect/ │ │ └── LoginLogAspect.java │ ├── controller/ │ │ ├── AdminTwoFAController.java │ │ ├── UserController.java │ │ └── UserManagerController.java │ ├── event/ │ │ ├── UserCopyEvent.java │ │ └── UserDeleteEvent.java │ ├── manager/ │ │ └── UserManager.java │ ├── mapper/ │ │ ├── UserMapper.java │ │ └── UserStorageSourceMapper.java │ ├── model/ │ │ ├── constant/ │ │ │ └── UserConstant.java │ │ ├── dto/ │ │ │ └── UserStorageSourceDetailDTO.java │ │ ├── entity/ │ │ │ ├── User.java │ │ │ └── UserStorageSource.java │ │ ├── enums/ │ │ │ ├── LoginLogModeEnum.java │ │ │ └── LoginVerifyModeEnum.java │ │ ├── request/ │ │ │ ├── CheckUserDuplicateRequest.java │ │ │ ├── CopyUserRequest.java │ │ │ ├── QueryUserRequest.java │ │ │ ├── ResetAdminUserNameAndPasswordRequest.java │ │ │ ├── SaveUserRequest.java │ │ │ ├── UpdateUserPwdRequest.java │ │ │ ├── UserLoginRequest.java │ │ │ └── VerifyLoginTwoFactorAuthenticatorRequest.java │ │ ├── response/ │ │ │ └── UserDetailResponse.java │ │ └── result/ │ │ ├── CheckLoginResult.java │ │ ├── LoginResult.java │ │ ├── LoginTwoFactorAuthenticatorResult.java │ │ └── LoginVerifyImgResult.java │ ├── service/ │ │ ├── DynamicLoginEntryService.java │ │ ├── UserService.java │ │ ├── UserStorageSourceService.java │ │ └── login/ │ │ ├── ImgVerifyCodeService.java │ │ ├── LoginService.java │ │ ├── TwoFactorAuthenticatorVerifyService.java │ │ └── verify/ │ │ ├── LoginVerifyService.java │ │ └── impl/ │ │ ├── ImgCodeLoginVerifyService.java │ │ ├── PasswordVerifyService.java │ │ └── TwoFactorAuthLoginVerifyService.java │ ├── util/ │ │ └── LoginEntryPathUtils.java │ └── utils/ │ └── PasswordVerifyUtils.java └── resources/ ├── META-INF/ │ └── additional-spring-configuration-metadata.json ├── application-default.properties ├── application-dev.properties ├── application-prod.properties ├── application.properties ├── banner.txt ├── db/ │ ├── migration-mysql/ │ │ ├── R__data.sql │ │ ├── V10__system_config_add_field_webdav.sql │ │ ├── V11__system_config_modify_field_only_office_url_to_https.sql │ │ ├── V12__system_config_modify_field_value_to_text.sql │ │ ├── V13__system_config_add_field_allow_path_link_anon_access.sql │ │ ├── V14__system_config_add_field_load_more_size.sql │ │ ├── V15__system_config_add_field_site_home_name.sql │ │ ├── V16__system_config_add_field_default_sort_field.sql │ │ ├── V17__system_config_add_field_link_limit_field.sql │ │ ├── V18__download_log_add_field_download_type.sql │ │ ├── V19__short_link_add_field_expire_date.sql │ │ ├── V1__Base_version.sql │ │ ├── V20__system_config_add_field_favicon_url_field.sql │ │ ├── V21__system_config_add_field_expire_times_field.sql │ │ ├── V22__system_config_add_field_default_save_pwd_field.sql │ │ ├── V23__system_config_add_field_only_office_secret_field.sql │ │ ├── V24__system_config_add_field_enable_hover_menu_field.sql │ │ ├── V25__system_config_add_field_site_access_field.sql │ │ ├── V26__system_config_add_field_login_verify.sql │ │ ├── V27__add_table_login_log.sql │ │ ├── V28__add_multi_user.sql │ │ ├── V29__system_config_add_field_login_verify.sql │ │ ├── V2__download_log_modify_storage_key_field_length.sql │ │ ├── V30__delete_storage_source_auto_cors_config.sql │ │ ├── V31__system_config_add_field_webdav.sql │ │ ├── V32__system_config_delete_domain_field.sql │ │ ├── V33__storage_source_config_update_field.sql │ │ ├── V34__storage_source_config_update_field.sql │ │ ├── V35__system_config_add_field_login_log_mode.sql │ │ ├── V36__user_add_field_salt.sql │ │ ├── V37__set_login_log_model_default_off.sql │ │ ├── V38__update_login_log_ip_field_length.sql │ │ ├── V3__system_config_add_field_file_click_mode.sql │ │ ├── V40__system_config_add_field_mobile_layout.sql │ │ ├── V41__system_config_add_custom_office_suffix.sql │ │ ├── V42__system_config_add_guest_index_html.sql │ │ ├── V43__set_2fa_default_value.sql │ │ ├── V44__system_config_add_mobile_.sql │ │ ├── V45__add_sso_config.sql │ │ ├── V46__add_template_user.sql │ │ ├── V47__system_config_add_force_backend_address.sql │ │ ├── V48__system_config_add_field_kkfileview_url.sql │ │ ├── V49__system_config_add_custom_kkfileview_suffix.sql │ │ ├── V4__download_log_modify_ip_field_length.sql │ │ ├── V50__system_config_add_kkfileview_open_mode.sql │ │ ├── V51__storage_source_config_add_refresh_token_expired_at.sql │ │ ├── V52__ststem_config_add_mobile_show_file_size.sql │ │ ├── V53__readme_config_add_path_mode_field.sql │ │ ├── V54__add_share_link_table.sql │ │ ├── V55__system_config_add_secure_login_entry.sql │ │ ├── V56__system_config_add_download_confirm_flags.sql │ │ ├── V57__user_add_default_share_permissions.sql │ │ ├── V5__add_permission_config_table.sql │ │ ├── V6__system_config_add_field_auth_code.sql │ │ ├── V7__system_config_add_field_max_file_uploads.sql │ │ ├── V8__storage_source_add_field_compatibility_readme.sql │ │ └── V9__system_config_add_field_only_office_url.sql │ └── migration-sqlite/ │ ├── R__data.sql │ ├── V10__system_config_add_field_webdav.sql │ ├── V11__system_config_modify_field_only_office_url_to_https.sql │ ├── V12__system_config_modify_field_value_to_text.sql │ ├── V13__system_config_add_field_allow_path_link_anon_access.sql │ ├── V14__system_config_add_field_load_more_size.sql │ ├── V15__system_config_add_field_site_home_name.sql │ ├── V16__system_config_add_field_default_sort_field.sql │ ├── V17__system_config_add_field_link_limit_field.sql │ ├── V18__download_log_add_field_download_type.sql │ ├── V19__short_link_add_field_expire_date.sql │ ├── V1__Base_version.sql │ ├── V20__system_config_add_field_favicon_url_field.sql │ ├── V21__system_config_add_field_expire_times_field.sql │ ├── V22__system_config_add_field_default_save_pwd_field.sql │ ├── V23__system_config_add_field_only_office_secret_field.sql │ ├── V24__system_config_add_field_enable_hover_menu_field.sql │ ├── V25__system_config_add_field_site_access_field.sql │ ├── V26__system_config_add_field_login_verify.sql │ ├── V27__add_table_login_log.sql │ ├── V28__add_multi_user.sql │ ├── V29__system_config_add_field_login_verify.sql │ ├── V2__download_log_modify_storage_key_field_length.sql │ ├── V30__delete_storage_source_auto_cors_config.sql │ ├── V31__system_config_add_field_webdav.sql │ ├── V32__system_config_delete_domain_field.sql │ ├── V33__storage_source_config_update_field.sql │ ├── V34__storage_source_config_update_field.sql │ ├── V35__system_config_add_field_login_log_mode.sql │ ├── V36__user_add_field_salt.sql │ ├── V37__fix_user_create_time_field_to_timestamp.sql │ ├── V38__set_login_log_model_default_off.sql │ ├── V3__system_config_add_field_file_click_mode.sql │ ├── V40__system_config_add_field_mobile_layout.sql │ ├── V41__system_config_add_custom_office_suffix.sql │ ├── V42__system_config_add_guest_index_html.sql │ ├── V43__set_2fa_default_value.sql │ ├── V44__system_config_add_mobile_.sql │ ├── V45__add_sso_config.sql │ ├── V46__add_template_user.sql │ ├── V47__system_config_add_force_backend_address.sql │ ├── V48__system_config_add_field_kkfileview_url.sql │ ├── V49__system_config_add_custom_kkfileview_suffix.sql │ ├── V4__download_log_modify_ip_field_length.sql │ ├── V50__system_config_add_kkfileview_open_mode.sql │ ├── V51__storage_source_config_add_refresh_token_expired_at.sql │ ├── V52__ststem_config_add_mobile_show_file_size.sql │ ├── V53__readme_config_add_path_mode_field.sql │ ├── V54__add_share_link_table.sql │ ├── V55__system_config_add_secure_login_entry.sql │ ├── V56__system_config_add_download_confirm_flags.sql │ ├── V57__user_add_default_share_permissions.sql │ ├── V5__add_permission_config_table.sql │ ├── V6__system_config_add_field_auth_code.sql │ ├── V7__system_config_add_field_max_file_uploads.sql │ ├── V8__storage_source_add_field_compatibility_readme.sql │ └── V9__system_config_add_field_only_office_url.sql ├── logback-spring.xml ├── mapper/ │ ├── DownloadLogMapper.xml │ ├── FilterConfigMapper.xml │ ├── LoginLogMapper.xml │ ├── PasswordConfigMapper.xml │ ├── PermissionConfigMapper.xml │ ├── ReadmeConfigMapper.xml │ ├── ShareLinkMapper.xml │ ├── ShortLinkMapper.xml │ ├── SsoConfigMapper.xml │ ├── StorageConfigMapper.xml │ ├── StorageSourceMapper.xml │ ├── SystemConfigMapper.xml │ ├── UserMapper.xml │ └── UserStorageSourceMapper.xml └── templates/ ├── callback.html └── error/ └── 404.html ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: 'Blank Issue' description: 请使用 https://issue.zfile.vip 创建新的问题. body: - type: markdown attributes: value: | **注意:** 不要通过此页面创建问题, 请使用 https://issue.zfile.vip 创建新的问题. 如果不是通过此链接创建的问题, 将会被直接关闭. - type: textarea id: add-a-description attributes: label: Add a description ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true contact_links: - name: 创建 Issue url: https://issue.zfile.vip/ about: 未通过 https://issue.zfile.vip/ 创建的问题可能会被立即关闭。 ================================================ FILE: .gitignore ================================================ HELP.md target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ !**/src/test/**/target/ ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr .fastRequest .murphy.yml ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ build/ !**/src/main/**/build/ !**/src/test/**/build/ ### VS Code ### .vscode/ /.mvn/wrapper/ /mvnw /mvnw.cmd /result/ ================================================ FILE: .package/script/log.sh ================================================ #!/bin/bash tail -fn100 ~/.zfile-v4/logs/zfile.log ================================================ FILE: .package/script/restart.sh ================================================ #!/bin/bash DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" $DIR/stop.sh $DIR/start.sh ================================================ FILE: .package/script/start.sh ================================================ #!/bin/bash # 检测是否已启动 pid=`ps -ef | grep -n zfile | grep -v grep | grep -v launch | grep -v .sh | awk '{print $2}'` if [ -n "${pid}" ] then echo "已运行在 pid:${pid},无需重复启动!" exit 0 fi # 获取当前脚本所在路径 DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" ZFILE_DIR=$(dirname "$DIR") # 启动 zfile nohup $ZFILE_DIR/zfile/zfile --spring.config.location=$ZFILE_DIR/application.properties --spring.web.resources.static-locations=file:$ZFILE_DIR/static/ >/dev/null 2>&1 & echo '启动中...' sleep 3s # 输出 pid pid=`ps -ef | grep -n zfile | grep -v grep | grep -v .sh | awk '{print $2}'` echo "目前 PID 为: ${pid}" ================================================ FILE: .package/script/status.sh ================================================ #!/bin/bash echo "------------------ 检测状态 START --------------" pid=`ps -ef | grep -n zfile | grep -v grep | grep -v launch | grep -v .sh | awk '{print $2}'` if [ -z "${pid}" ] then echo "未运行, 无需停止!" else echo "运行pid:${pid}" fi echo "------------------ 检测状态 END --------------" ================================================ FILE: .package/script/stop.sh ================================================ #!/bin/bash echo "------------------ 检测状态 START --------------" pid=`ps -ef | grep -n zfile | grep -v grep | grep -v .sh | awk '{print $2}'` if [ -z "${pid}" ] then echo "未运行, 无需停止!" else echo "运行pid:${pid}" kill -9 ${pid} echo "已停止进程: ${pid}" fi echo "------------------ 检测状态 END --------------" ================================================ FILE: .package/script/双击我启动.bat ================================================ @echo off if not exist %windir%\system32\cmd.exe ( "%CD%\zfile\zfile.exe" ) else ( cmd /k "%CD%\zfile\zfile.exe" exit ) ================================================ FILE: Dockerfile ================================================ # 此文件仅作为示例使用,与 ZFile 实际打包的 Dockerfile 不同(采用 Graal Native 打包,这部分不开源) FROM maven:3.9.9-eclipse-temurin-21-alpine AS builder WORKDIR /root ADD ./pom.xml pom.xml ADD ./src src RUN mvn clean package -Dmaven.test.skip=true FROM ibm-semeru-runtimes:open-21-jre-jammy WORKDIR /root EXPOSE 8080 ENV LANG=C.UTF-8 ENV LC_ALL=C.UTF-8 RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime RUN echo 'Asia/Shanghai' >/etc/timezone RUN apt update -y && apt install --no-install-recommends fontconfig -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* COPY --from=builder /root/target/*.jar /root/app.jar CMD ["java", "-jar", "app.jar"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 ZhaoJun Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================
ZFile

ZFile 是一个适用于个人或小团队的在线网盘程序,可以将多种存储类型统一管理,再也不用登录各种网站管理文件,现在你只需要在 ZFile 中畅快使用!

last commit downloads release version commit activity open issues closed issues forks stars watchers gitcode
官网 | 文档 | 预览地址
## 系统特色 - Docker、Docker Compose 支持(amd64, arm64)。 - 支持对文件生成直链、短链(可设过期时间)。 - 响应式设计,支持手机、平板、电脑等多种设备访问。 - 支持多用户功能,可分配给指定用户指定存储源或目录。 - 支持在线浏览图片、播放音视频,文本文件、Office、Obj(3d)等文件类型。 - 支持对接 S3、OneDrive、SharePoint、Google Drive、多吉云、又拍云、本地存储、FTP、SFTP 等存储源。 - 支持常用快捷键,`Ctrl + A` 全选,`Ctrl + 左键` 多选,`Shift + 左键` 范围选择,`Esc` 取消全选等。 - 支持限速下载(捐赠版) - 支持限制指定用户可查看、上传的文件类型(捐赠版) ## 快速开始 一键脚本安装: ```bash curl -sSL https://docs.zfile.vip/install.sh -o install.sh && chmod +x install.sh && ./install.sh ``` 更多安装方式请参考 [安装文档](https://docs.zfile.vip/install/) ## 功能预览 ### 文件列表 ![文件列表](/img/file-list.png) ### 画廊模式 ![图片预览](/img/gallery.png) ### 视频预览 ![视频预览](/img/preview-video.png) ### 文本预览 ![文本预览](/img/preview-text.png) ### 音频预览 ![音频预览](/img/preview-audio.png) ### PDF 预览 ![PDF 预览](/img/preview-pdf.png) ### Office 预览 ![Office 预览](/img/preview-office.png) ### 3d 文件预览 ![3d 文件预览](/img/preview-3d.png) ### 生成直链 ![生成直链](/img/generate-link.jpeg) ### 页面设置 ![页面设置](/img/page-setting.png) ### 后台设置-登录 ![后台设置-登录](/img/login.png) ### 后台设置-存储源列表 ![后台设置-存储源列表](/img/storage-list.png) ### 后台设置-添加存储源(本地存储) ![后台设置-添加存储源(本地存储)](/img/storage-edit-local.png) ### 后台设置-用户管理 ![后台设置-存储源权限控制](/img/user-edit.png) ### 后台设置-显示设置 ![后台设置-显示设置](/img/view-setting.png) ## 支持作者 如果本项目对你有帮助,请作者喝杯咖啡吧。 赞助我 ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=zfile-dev/zfile&type=Date)](https://star-history.com/#zfile-dev/zfile&Date) ================================================ FILE: pom.xml ================================================ 4.0.0 im.zhaojun zfile 4.5.0 zfile jar 一个在线的文件浏览系统 org.springframework.boot spring-boot-starter-parent 3.3.2 true 21 21 21 UTF-8 UTF-8 UTF-8 1.5.3.Final 2.0 2.14.1 3.46.0.1 10.12.0 1.18.32 software.amazon.awssdk bom 2.24.3 pom import org.graalvm.sdk graal-sdk 24.1.0 provided org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-configuration-processor true org.springframework.boot spring-boot-starter-data-redis org.springframework.boot spring-boot-starter-cache org.springframework.boot spring-boot-starter-thymeleaf org.springframework.boot spring-boot-starter-validation com.mysql mysql-connector-j runtime org.xerial sqlite-jdbc org.flywaydb flyway-core ${flyway.version} org.flywaydb flyway-mysql ${flyway.version} com.baomidou mybatis-plus-spring-boot3-starter 3.5.6 com.upyun java-sdk 4.2.3 software.amazon.awssdk s3 com.qiniu qiniu-java-sdk 7.12.1 com.github.mwiede jsch 0.2.20 com.github.lookfirst sardine 5.12 org.slf4j slf4j-simple cn.dev33 sa-token-spring-boot3-starter 1.38.0 com.github.xiaoymin knife4j-openapi3-jakarta-spring-boot-starter 4.5.0 cn.hutool hutool-all 5.8.31 org.apache.poi poi-ooxml 5.4.0 org.apache.commons commons-compress org.apache.commons commons-compress 1.26.2 compile org.projectlombok lombok provided commons-net commons-net 3.11.0 org.apache.commons commons-pool2 com.squareup.okhttp3 okhttp com.alibaba.fastjson2 fastjson2 2.0.29 com.google.guava guava 33.3.0-jre org.mapstruct mapstruct ${org.mapstruct.version} commons-chain commons-chain 1.2 dev.samstevens.totp totp 1.7.1 com.google.zxing core com.google.zxing javase org.json json 20231013 org.apache.httpcomponents httpmime 4.5.13 org.apache.httpcomponents.client5 httpclient5 org.bouncycastle bcprov-jdk15on 1.70 org.springframework.retry spring-retry commons-fileupload commons-fileupload 1.6.0 com.alibaba dns-cache-manipulator 1.8.2 com.github.oshi oshi-core 6.6.3 org.springframework.boot spring-boot-maven-plugin org.apache.maven.plugins maven-compiler-plugin 21 21 UTF-8 org.mapstruct mapstruct-processor ${org.mapstruct.version} org.projectlombok lombok 1.18.32 org.projectlombok lombok-mapstruct-binding 0.2.0 org.flywaydb flyway-maven-plugin ================================================ FILE: src/main/java/im/zhaojun/zfile/ZfileApplication.java ================================================ package im.zhaojun.zfile; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.ServletComponentScan; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.EnableAspectJAutoProxy; /** * @author zhaojun */ @SpringBootApplication @EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true) @ServletComponentScan(basePackages = {"im.zhaojun.zfile.core.filter", "im.zhaojun.zfile.module.storage.filter"}) @ComponentScan(basePackages = "im.zhaojun.zfile.*") public class ZfileApplication { public static void main(String[] args) { SpringApplication.run(ZfileApplication.class, args); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/annotation/ApiLimit.java ================================================ package im.zhaojun.zfile.core.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.concurrent.TimeUnit; /** * 接口限流注解 * * @author zhaojun */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ApiLimit { /** * 持续时间 */ int timeout(); /** * 时间单位, 默认为秒 */ TimeUnit timeUnit() default TimeUnit.SECONDS; /** * 单位时间内允许访问的最大次数 */ long maxCount(); } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/annotation/DemoDisable.java ================================================ package im.zhaojun.zfile.core.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 演示系统禁用功能注解 * * @author zhaojun */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface DemoDisable { } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/aspect/ApiLimitAspect.java ================================================ package im.zhaojun.zfile.core.aspect; import cn.hutool.cache.CacheUtil; import cn.hutool.cache.impl.TimedCache; import cn.hutool.extra.servlet.JakartaServletUtil; import im.zhaojun.zfile.core.annotation.ApiLimit; import im.zhaojun.zfile.core.exception.ErrorCode; import im.zhaojun.zfile.core.exception.core.BizException; import im.zhaojun.zfile.core.util.RequestHolder; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; /** * 接口限流切面, 通过注解 {@link ApiLimit} 进行限流. * * @author zhaojun */ @Aspect @Component public class ApiLimitAspect { private final TimedCache apiLimitTimedCache = CacheUtil.newTimedCache(1000); public static final String API_LIMIT_KEY_PREFIX = "api_limit_"; /** * 在标记了 {@link ApiLimit} 注解的方法执行前进行限流校验. * * @param joinPoint 切点 */ @Before("@annotation(apiLimit)") public void before(JoinPoint joinPoint, ApiLimit apiLimit) { // 获取当前请求的方法上的注解中设置的值 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); // 反射获取当前被调用的方法 Method method = signature.getMethod(); int timeout = apiLimit.timeout(); TimeUnit timeUnit = apiLimit.timeUnit(); long millis = timeUnit.toMillis(timeout); long maxCount = apiLimit.maxCount(); // 获取请求相关信息 String ip = JakartaServletUtil.getClientIP(RequestHolder.getRequest()); // 限制访问次数 String key = API_LIMIT_KEY_PREFIX.concat(ip).concat(method.getName()); AtomicLong atomicLong = apiLimitTimedCache.get(key, false); if (atomicLong == null) { apiLimitTimedCache.put(key, new AtomicLong(1), millis); } else { if (atomicLong.incrementAndGet() > maxCount) { throw new BizException(ErrorCode.BIZ_ACCESS_TOO_FREQUENT); } } } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/aspect/CommonResultControllerAdvice.java ================================================ package im.zhaojun.zfile.core.aspect; import im.zhaojun.zfile.core.constant.MdcConstant; import im.zhaojun.zfile.core.util.AjaxJson; import org.slf4j.MDC; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; import org.springframework.http.converter.json.MappingJacksonValue; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; /** * Controller 切面, 用于处理返回值统一封装. * * @author zhaojun */ @ControllerAdvice public class CommonResultControllerAdvice implements ResponseBodyAdvice { @Override public boolean supports(MethodParameter returnType, @NonNull Class> converterType) { return AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType); } @Override @NonNull public final Object beforeBodyWrite(@Nullable Object body, @NonNull MethodParameter returnType, @NonNull MediaType contentType, @NonNull Class> converterType, @NonNull ServerHttpRequest request, @NonNull ServerHttpResponse response) { MappingJacksonValue container = getOrCreateContainer(body); // The contain body will never be null beforeBodyWriteInternal(container, contentType, returnType, request, response); return container; } /** * Wrap the body in a {@link MappingJacksonValue} value container (for providing * additional serialization instructions) or simply cast it if already wrapped. */ private MappingJacksonValue getOrCreateContainer(Object body) { return body instanceof MappingJacksonValue ? (MappingJacksonValue) body : new MappingJacksonValue(body); } private void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType, MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) { // Get return body Object returnBody = bodyContainer.getValue(); if (returnBody instanceof AjaxJson baseResponse) { // 将 MDC 中的 TraceId 设置到返回值中 baseResponse.setTraceId(MDC.get(MdcConstant.TRACE_ID)); } } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/aspect/DemoDisableAspect.java ================================================ package im.zhaojun.zfile.core.aspect; import im.zhaojun.zfile.core.annotation.DemoDisable; import im.zhaojun.zfile.core.config.ZFileProperties; import im.zhaojun.zfile.core.exception.ErrorCode; import im.zhaojun.zfile.core.exception.core.BizException; import jakarta.annotation.Resource; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; /** * 通过注解 {@link DemoDisable} 限制演示系统不可操作的功能. * * @author zhaojun */ @Aspect @Component public class DemoDisableAspect { @Resource private ZFileProperties zFileProperties; /** * 定义一个切点(通过注解) */ @Pointcut("@annotation(im.zhaojun.zfile.core.annotation.DemoDisable)") public void demoDisable() { } /** * 在标记了 {@link DemoDisable} 注解的方法执行前进行限流校验. * * @param joinPoint 切点 */ @Before("demoDisable()") public void before(JoinPoint joinPoint) { if (zFileProperties.isDemoSite()) { throw new BizException(ErrorCode.DEMO_SITE_DISABLE_OPERATOR); } } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/cache/ZFileCacheManager.java ================================================ package im.zhaojun.zfile.core.cache; import im.zhaojun.zfile.module.storage.model.entity.StorageSource; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; /** * ZFile 业务缓存,针对无法使用 jsr-107 的缓存进行处理的业务逻辑。 */ @Component public class ZFileCacheManager { /** * 用户可用的存储源列表缓存 */ private static final Map> userEnableStorageSourceCache = new ConcurrentHashMap<>(); /** * 根据用户 ID 获取可用的存储源列表,若缓存中不存在,则通过 mappingFunction 获取并返回。 * * @param userId * 用户 ID * * @param mappingFunction * 当缓存中不存在时,用于获取存储源列表的函数。 * * @return 存储源列表函数 */ public List findAllEnableOrderByOrderNum(Integer userId, Function> mappingFunction) { return userEnableStorageSourceCache.computeIfAbsent(userId, mappingFunction); } /** * 清空所有用户的存储源缓存。 */ public void clearUserEnableStorageSourceCache() { userEnableStorageSourceCache.clear(); } /** * 清除指定用户的存储源缓存。 * * @param userId * 用户 ID */ public void clearUserEnableStorageSourceCache(Integer userId) { userEnableStorageSourceCache.remove(userId); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/ZFileProperties.java ================================================ package im.zhaojun.zfile.core.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.stereotype.Component; /** * ZFile 配置类,将配置文件中的 zfile 配置项映射到该类中. * * @author zhaojun */ @Data @EnableConfigurationProperties @Component @ConfigurationProperties(prefix = "zfile") public class ZFileProperties { private boolean debug; private String version; private boolean isDemoSite; private OAuth2Properties onedrive = new OAuth2Properties(); private OAuth2Properties onedriveChina = new OAuth2Properties(); private OAuth2Properties gd = new OAuth2Properties(); private Open115Properties open115 = new Open115Properties(); @Data public static class OAuth2Properties { private String clientId; private String clientSecret; private String redirectUri; private String scope; } @Data public static class Open115Properties { private String appId; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/datasource/DataSourceBeanPostProcessor.java ================================================ package im.zhaojun.zfile.core.config.datasource; import cn.hutool.core.io.FileUtil; import cn.hutool.extra.spring.SpringUtil; import com.zaxxer.hikari.HikariDataSource; import im.zhaojun.zfile.core.util.StringUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.flyway.FlywayProperties; import org.springframework.core.PriorityOrdered; import org.springframework.stereotype.Component; import java.io.File; import java.util.List; /** * 在 Spring 容器初始化时, 对数据源进行处理. *
* 1. 针对 DataSource 进行处理,仅针对 sqlite: *
    *
  • 提前创建 sqlite 数据文件所在目录.
  • *
  • 检测到版本更新时(pom.xml -> project.version)自动备份原数据库.
  • *
*
* 2. 针对 Flyway 进行处理,根据数据库类型, 配置不同的 Flyway Migration Location: *
    *
  • SQLite 数据库使用 migration-sqlite 目录.
  • *
  • MySQL 数据库使用 migration-mysql 目录.
  • *
* * @author zhaojun */ @Slf4j @Component public class DataSourceBeanPostProcessor implements BeanPostProcessor, PriorityOrdered { public static final String ZFILE_VERSION_PROPERTIES = "zfile.db.version"; public static final String DRIVE_CLASS_NAME_PROPERTIES = "spring.datasource.driver-class-name"; public static final String DATA_SOURCE_BEAN_NAME = "dataSource"; public static final String SQLITE_DRIVE_CLASS_NAME = "org.sqlite.JDBC"; public static final String MYSQL_DRIVE_CLASS_NAME = "com.mysql.cj.jdbc.Driver"; @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { // 如果更改了数据源类型这里要修改 if (bean instanceof HikariDataSource dataSource && DATA_SOURCE_BEAN_NAME.equals(beanName)) { processSqliteDataSource(dataSource); } else if (bean instanceof FlywayProperties flywayProperties) { processFlywayLocations(flywayProperties); } return bean; } /** * 如果是 sqlite 数据库, 提前创建数据库文件所在目录.
* * 如果检测到版本更新, 自动备份原数据库文件. * * @param dataSource * 数据源 */ private void processSqliteDataSource(HikariDataSource dataSource) { String driverClassName = dataSource.getDriverClassName(); String jdbcUrl = dataSource.getJdbcUrl(); if (StringUtils.equals(driverClassName, SQLITE_DRIVE_CLASS_NAME)) { String path = jdbcUrl.replace("jdbc:sqlite:", ""); String folderPath = FileUtil.getAbsolutePath(new File(path).getParentFile()); log.info("SQLite 数据库文件所在目录: [{}]", folderPath); File file = new File(folderPath); if (!file.exists()) { log.info("检测到 SQLite 数据库文件所在目录不存在, 已自动创建."); if (!file.mkdirs()) { log.error("SQLite 数据库文件创建失败."); } } else { log.info("检测到 SQLite 数据库文件所在目录已存在, 无需自动创建."); // 更新版本时, 先自动备份数据库文件 String version = SpringUtil.getProperty(ZFILE_VERSION_PROPERTIES); if (StringUtils.isNotEmpty(version)) { String backupPath = folderPath + "/zfile-update-" + version + "-backup.db"; if (!FileUtil.exist(path)) { log.error("检测到 SQLite 数据库文件不存在, 一般为初始化状态,无需备份."); return; } if (FileUtil.exist(backupPath)) { log.info("检测到 SQLite 数据库备份文件 [{}] 已存在, 无需再次备份.", backupPath); } else { FileUtil.copy(path, backupPath, false); log.info("自动备份 SQLite 数据库文件到: [{}]", backupPath); } } } } } /** * 根据使用的不同数据库, 配置使用不同的 migration location * * @param flywayProperties * flyway 配置项 */ private void processFlywayLocations(FlywayProperties flywayProperties) { String driveClassName = SpringUtil.getProperty(DRIVE_CLASS_NAME_PROPERTIES); if (SQLITE_DRIVE_CLASS_NAME.equals(driveClassName)) { flywayProperties.setLocations(List.of("classpath:db/migration-sqlite")); } else if (MYSQL_DRIVE_CLASS_NAME.equals(driveClassName)) { flywayProperties.setLocations(List.of("classpath:db/migration-mysql")); } } @Override public int getOrder() { return Integer.MIN_VALUE; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/docs/Knife4jConfiguration.java ================================================ package im.zhaojun.zfile.core.config.docs; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; import io.swagger.v3.oas.models.media.StringSchema; import io.swagger.v3.oas.models.parameters.HeaderParameter; import org.springdoc.core.customizers.OperationCustomizer; import org.springdoc.core.models.GroupedOpenApi; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * Knife4j 参数配置,区分前台功能和管理员功能,并为管理员接口增加统一 token header 配置. * * @author zhaojun */ @Configuration public class Knife4jConfiguration { @Bean public GroupedOpenApi groupedOpenApi() { String groupName = "前台功能"; return GroupedOpenApi.builder() .group(groupName) .packagesToScan("im.zhaojun.zfile.module") .pathsToExclude("/admin/**") .build(); } @Bean public GroupedOpenApi groupedOpenApi2() { String groupName = "管理员功能"; return GroupedOpenApi.builder() .group(groupName) .packagesToScan("im.zhaojun.zfile.module") .pathsToMatch("/admin/**") .addOperationCustomizer(globalOperationCustomizer()) .build(); } public OperationCustomizer globalOperationCustomizer() { return (operation, handlerMethod) -> { operation.addParametersItem(new HeaderParameter() .name("zfile-token") .description("token") .required(true) .schema(new StringSchema())); return operation; }; } @Bean public OpenAPI customOpenAPI() { Contact contact = new Contact(); contact.setName("zhaojun"); contact.setUrl("https://zfile.vip"); contact.setEmail("873019219@qq.com"); return new OpenAPI() .info(new Info() .title("ZFILE 文档") .description("# 这是 ZFILE Restful 接口文档展示页面") .termsOfService("https://www.zfile.vip") .contact(contact) .version("1.0") .license(new License() .name("Apache 2.0") .url("http://doc.xiaominfo.com"))); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/jackson/JSONStringDeserializer.java ================================================ package im.zhaojun.zfile.core.config.jackson; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; /** * JSON String 反序列化器, 用于将 JSON 字符串反序列化为 JSON 对象. * * @author zhaojun */ public class JSONStringDeserializer extends JsonDeserializer { @Override public String deserialize(JsonParser p, DeserializationContext context) throws IOException { JsonNode node = p.getCodec().readTree(p); ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(node); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/jackson/JSONStringSerializer.java ================================================ package im.zhaojun.zfile.core.config.jackson; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import java.io.IOException; /** * JSON String 序列化器, 用于将 JSON 字符串序列化为 JSON 对象. * * @author zhaojun */ public class JSONStringSerializer extends JsonSerializer { @Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeRawValue(value); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/mybatis/CollectionIntegerTypeHandler.java ================================================ package im.zhaojun.zfile.core.config.mybatis; import java.util.Set; public class CollectionIntegerTypeHandler extends CollectionTypeHandler> { } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/mybatis/CollectionStrTypeHandler.java ================================================ package im.zhaojun.zfile.core.config.mybatis; import java.util.Set; public class CollectionStrTypeHandler extends CollectionTypeHandler> { } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/mybatis/CollectionTypeHandler.java ================================================ package im.zhaojun.zfile.core.config.mybatis; import cn.hutool.core.convert.Convert; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.StrUtil; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedJdbcTypes; import org.apache.ibatis.type.MappedTypes; import org.springframework.core.ResolvableType; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; /** * 自定义 Set 类型处理器, 用于处理数据库 VARCHAR 类型字段和 Java Set 类型属性之间的转换. * 支持字符串格式为: "[a, b, c]". * * @author zhaojun */ @MappedJdbcTypes(JdbcType.VARCHAR) public abstract class CollectionTypeHandler extends BaseTypeHandler { @Override public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException { if (parameter instanceof Collection collection) { StringJoiner joiner = new StringJoiner(","); for (Object o : collection) { joiner.add(Convert.toStr(o)); } ps.setString(i, joiner.toString()); } else { ps.setString(i, Convert.toStr(parameter)); } } @Override public Object getNullableResult(ResultSet rs, String columnName) throws SQLException { String str = rs.getString(columnName); return convertToEntityAttribute(str); } @Override public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException { String str = rs.getString(columnIndex); return convertToEntityAttribute(str); } @Override public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { String str = cs.getString(columnIndex); return convertToEntityAttribute(str); } private Class collectionClazz; private Type innerType; /** * 构造方法 */ public CollectionTypeHandler() { ResolvableType resolvableType = ResolvableType.forClass(getClass()); Type type = resolvableType.as(CollectionTypeHandler.class).getGeneric().getType(); if (type instanceof ParameterizedType parameterizedType) { collectionClazz = (Class) parameterizedType.getRawType(); // 获取实际类型参数(泛型参数,例如 List 中的 String) Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); // 使用这些信息做进一步操作 for (Type actualTypeArgument : actualTypeArguments) { innerType = actualTypeArgument; break; } } } private Object convertToEntityAttribute(String dbData) { if (StrUtil.isEmpty(dbData)) { if (List.class.isAssignableFrom(collectionClazz)) { return Collections.emptyList(); } else if (Set.class.isAssignableFrom(collectionClazz)) { return Collections.emptySet(); } else { return null; } } Collection collection; if (List.class.isAssignableFrom(collectionClazz)) { collection = new ArrayList<>(); } else if (Set.class.isAssignableFrom(collectionClazz)) { collection = new HashSet<>(); } else { return null; } String[] split = dbData.split(","); for (String s : split) { if (NumberUtil.isNumber(s)) { collection.add(Convert.convert(Integer.class, s)); } else { collection.add(s); } } return collection; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/mybatis/MyBatisPlusConfig.java ================================================ package im.zhaojun.zfile.core.config.mybatis; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; import java.sql.SQLException; /** * mybatis-plus 配置类 * * @author zhaojun */ @Configuration public class MyBatisPlusConfig { /** * mybatis plus 分页插件配置 */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor(DataSource dataSource) throws SQLException { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); String databaseProductName = dataSource.getConnection().getMetaData().getDatabaseProductName(); DbType dbType = DbType.getDbType(databaseProductName); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(dbType)); return interceptor; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/mybatis/MyDatabaseIdProvider.java ================================================ package im.zhaojun.zfile.core.config.mybatis; import org.apache.ibatis.mapping.DatabaseIdProvider; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; /** * MyBatis 数据库 ID Provider, 用于判断当前数据库类型来执行不同的 SQL 语句.
* 可在 xml 中使用 <if test="_databaseId = 'mysql'"> 来判断数据库类型.
* 也可以在外层使用,如 <delete id="xxx" databaseId="sqlite"> 来判断数据库类型. * * @author zhaojun */ @Component public class MyDatabaseIdProvider implements DatabaseIdProvider { private static final String DATABASE_MYSQL = "MySQL"; private static final String DATABASE_SQLITE = "SQLite"; @Override public String getDatabaseId(DataSource dataSource) throws SQLException { Connection conn = dataSource.getConnection(); String dbName = conn.getMetaData().getDatabaseProductName(); String dbAlias = ""; switch (dbName) { case DATABASE_MYSQL: dbAlias = "mysql"; break; case DATABASE_SQLITE: dbAlias = "sqlite"; break; default: break; } return dbAlias; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/mybatis/MyMetaObjectHandler.java ================================================ package im.zhaojun.zfile.core.config.mybatis; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.util.Date; /** * MyBatis Plus 自动填充配置类 * 用于自动填充 createTime 和 updateTime 字段 * * @author zhaojun */ @Slf4j @Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", Date.class, new Date()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date()); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/security/SaSessionForJacksonCustomized.java ================================================ package im.zhaojun.zfile.core.config.security; import cn.dev33.satoken.session.SaSession; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; /** * Jackson 定制版 SaSession,忽略 timeout 等属性的序列化 * * @author click33 * @since 1.34.0 */ @JsonIgnoreProperties({"timeout"}) public class SaSessionForJacksonCustomized extends SaSession { /** * */ private static final long serialVersionUID = -7600983549653130681L; public SaSessionForJacksonCustomized() { super(); } /** * 构建一个Session对象 * @param id Session的id */ public SaSessionForJacksonCustomized(String id) { super(id); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/security/SaTokenConfigure.java ================================================ package im.zhaojun.zfile.core.config.security; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.strategy.SaStrategy; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * SaToken 权限配置, 配置管理员才能访问管理员功能. * * @author zhaojun */ @Configuration public class SaTokenConfigure implements WebMvcConfigurer { /** * 注册权限校验拦截器, 拦截所有 /admin/** 请求,但不包含 /admin 因为这个是登录页面. * * @param registry * 拦截器注册器 */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle -> { SaRouter.match("/admin/**", () -> { StpUtil.checkLogin(); StpUtil.checkRole("admin"); }); })).addPathPatterns("/**").excludePathPatterns("/admin"); // 不再依赖 SaToken 的默认路径检查功能 SaStrategy.instance.checkRequestPath = (path, extArg1, extArg2) -> {}; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/security/SaTokenDaoRedisJackson.java ================================================ // // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package im.zhaojun.zfile.core.config.security; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.strategy.SaStrategy; import cn.dev33.satoken.util.SaFoxUtil; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.stereotype.Component; import java.lang.reflect.Field; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; /** * Sa-Token 持久层实现 [ Redis存储、Jackson序列化 ] * * @author click33 * @since 1.34.0 */ @Component @ConditionalOnProperty(name = "spring.data.redis.host") public class SaTokenDaoRedisJackson implements SaTokenDao { public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; public static final String DATE_PATTERN = "yyyy-MM-dd"; public static final String TIME_PATTERN = "HH:mm:ss"; public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN); public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_PATTERN); public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(TIME_PATTERN); /** * ObjectMapper 对象 (以 public 作用域暴露出此对象,方便开发者二次更改配置) * *

例如: *

     *      SaTokenDaoRedisJackson redisJackson = (SaTokenDaoRedisJackson) SaManager.getSaTokenDao();
     *      redisJackson.objectMapper.xxx = xxx;
     * 	
*

*/ public ObjectMapper objectMapper; /** * String 读写专用 */ public StringRedisTemplate stringRedisTemplate; /** * Object 读写专用 */ public RedisTemplate objectRedisTemplate; /** * 标记:是否已初始化成功 */ public boolean isInit; @Autowired public void init(RedisConnectionFactory connectionFactory) { // 如果已经初始化成功了,就立刻退出,不重复初始化 if(this.isInit) { return; } // 指定相应的序列化方案 StringRedisSerializer keySerializer = new StringRedisSerializer(); GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer(); // 通过反射获取Mapper对象, 增加一些配置, 增强兼容性 try { Field field = GenericJackson2JsonRedisSerializer.class.getDeclaredField("mapper"); field.setAccessible(true); this.objectMapper = (ObjectMapper) field.get(valueSerializer); // 配置[忽略未知字段] this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 配置[时间类型转换] JavaTimeModule timeModule = new JavaTimeModule(); // LocalDateTime序列化与反序列化 timeModule.addSerializer(new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); // LocalDate序列化与反序列化 timeModule.addSerializer(new LocalDateSerializer(DATE_FORMATTER)); timeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DATE_FORMATTER)); // LocalTime序列化与反序列化 timeModule.addSerializer(new LocalTimeSerializer(TIME_FORMATTER)); timeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(TIME_FORMATTER)); this.objectMapper.registerModule(timeModule); // 重写 SaSession 生成策略 SaStrategy.instance.createSession = (sessionId) -> new SaSessionForJacksonCustomized(sessionId); } catch (Exception e) { System.err.println(e.getMessage()); } // 构建StringRedisTemplate StringRedisTemplate stringTemplate = new StringRedisTemplate(); stringTemplate.setConnectionFactory(connectionFactory); stringTemplate.afterPropertiesSet(); // 构建RedisTemplate RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); template.setKeySerializer(keySerializer); template.setHashKeySerializer(keySerializer); template.setValueSerializer(valueSerializer); template.setHashValueSerializer(valueSerializer); template.afterPropertiesSet(); // 开始初始化相关组件 this.stringRedisTemplate = stringTemplate; this.objectRedisTemplate = template; // 打上标记,表示已经初始化成功,后续无需再重新初始化 this.isInit = true; } /** * 获取Value,如无返空 */ @Override public String get(String key) { return stringRedisTemplate.opsForValue().get(key); } /** * 写入Value,并设定存活时间 (单位: 秒) */ @Override public void set(String key, String value, long timeout) { if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { return; } // 判断是否为永不过期 if(timeout == SaTokenDao.NEVER_EXPIRE) { stringRedisTemplate.opsForValue().set(key, value); } else { stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS); } } /** * 修修改指定key-value键值对 (过期时间不变) */ @Override public void update(String key, String value) { long expire = getTimeout(key); // -2 = 无此键 if(expire == SaTokenDao.NOT_VALUE_EXPIRE) { return; } this.set(key, value, expire); } /** * 删除Value */ @Override public void delete(String key) { stringRedisTemplate.delete(key); } /** * 获取Value的剩余存活时间 (单位: 秒) */ @Override public long getTimeout(String key) { return stringRedisTemplate.getExpire(key); } /** * 修改Value的剩余存活时间 (单位: 秒) */ @Override public void updateTimeout(String key, long timeout) { // 判断是否想要设置为永久 if(timeout == SaTokenDao.NEVER_EXPIRE) { long expire = getTimeout(key); if(expire == SaTokenDao.NEVER_EXPIRE) { // 如果其已经被设置为永久,则不作任何处理 } else { // 如果尚未被设置为永久,那么再次set一次 this.set(key, this.get(key), timeout); } return; } stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS); } /** * 获取Object,如无返空 */ @Override public Object getObject(String key) { return objectRedisTemplate.opsForValue().get(key); } /** * 写入Object,并设定存活时间 (单位: 秒) */ @Override public void setObject(String key, Object object, long timeout) { if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { return; } // 判断是否为永不过期 if(timeout == SaTokenDao.NEVER_EXPIRE) { objectRedisTemplate.opsForValue().set(key, object); } else { objectRedisTemplate.opsForValue().set(key, object, timeout, TimeUnit.SECONDS); } } /** * 更新Object (过期时间不变) */ @Override public void updateObject(String key, Object object) { long expire = getObjectTimeout(key); // -2 = 无此键 if(expire == SaTokenDao.NOT_VALUE_EXPIRE) { return; } this.setObject(key, object, expire); } /** * 删除Object */ @Override public void deleteObject(String key) { objectRedisTemplate.delete(key); } /** * 获取Object的剩余存活时间 (单位: 秒) */ @Override public long getObjectTimeout(String key) { return objectRedisTemplate.getExpire(key); } /** * 修改Object的剩余存活时间 (单位: 秒) */ @Override public void updateObjectTimeout(String key, long timeout) { // 判断是否想要设置为永久 if(timeout == SaTokenDao.NEVER_EXPIRE) { long expire = getObjectTimeout(key); if(expire == SaTokenDao.NEVER_EXPIRE) { // 如果其已经被设置为永久,则不作任何处理 } else { // 如果尚未被设置为永久,那么再次set一次 this.setObject(key, this.getObject(key), timeout); } return; } objectRedisTemplate.expire(key, timeout, TimeUnit.SECONDS); } /** * 搜索数据 */ @Override public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { Set keys = stringRedisTemplate.keys(prefix + "*" + keyword + "*"); List list = new ArrayList<>(keys); return SaFoxUtil.searchList(list, start, size, sortType); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/security/StpInterfaceImpl.java ================================================ package im.zhaojun.zfile.core.config.security; import cn.dev33.satoken.stp.StpInterface; import cn.hutool.core.convert.Convert; import im.zhaojun.zfile.module.user.service.UserService; import jakarta.annotation.Resource; import org.springframework.stereotype.Component; import java.util.Collections; import java.util.List; /** * 自定义权限加载接口实现类 * * @author zhaojun */ @Component public class StpInterfaceImpl implements StpInterface { private static final List ADMIN_ROLE_LIST = Collections.singletonList("admin"); public static final List EMPTY_ROLE_LIST = Collections.emptyList(); @Resource private UserService userService; /** * 返回一个账号所拥有的权限码集合,这里没用到这个功能,所以返回空集合 */ @Override public List getPermissionList(Object loginId, String loginType) { return Collections.emptyList(); } /** * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验) */ @Override public List getRoleList(Object loginId, String loginType) { boolean isAdmin = userService.isAdmin(Convert.toInt(loginId)); return isAdmin ? ADMIN_ROLE_LIST : EMPTY_ROLE_LIST; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/spring/JacksonEnumDeserializer.java ================================================ package im.zhaojun.zfile.core.config.spring; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.BeanProperty; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.deser.ContextualDeserializer; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.jackson.JsonComponent; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Objects; /** * Jackson 枚举反序列化器, 用于将接收请求中的参数(一般为字符串)转换为枚举对象. * * @author zhaojun */ @Setter @Slf4j @JsonComponent public class JacksonEnumDeserializer extends JsonDeserializer> implements ContextualDeserializer { private Class clazz; /** * 反序列化操作 * * @param jsonParser * json 解析器 * * @param ctx * 反序列化上下文 * * @return 反序列化后的枚举值 * @throws IOException 反序列化异常 */ @Override public Enum deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException { Class enumType = clazz; if (Objects.isNull(enumType) || !enumType.isEnum()) { return null; } String text = jsonParser.getText(); Method method = StringToEnumConverterFactory.getMethod(clazz); Enum[] enumConstants = (Enum[]) enumType.getEnumConstants(); // 将值与枚举对象对应并缓存 for (Enum e : enumConstants) { try { if (Objects.equals(method.invoke(e).toString(), text)) { return e; } } catch (IllegalAccessException | InvocationTargetException ex) { log.error("获取枚举值错误!!! ", ex); } } return null; } /** * 为不同的枚举获取合适的解析器 * * @param ctx * 反序列化上下文 * * @param property * property */ @Override public JsonDeserializer> createContextual(DeserializationContext ctx, BeanProperty property) { Class rawCls = ctx.getContextualType().getRawClass(); JacksonEnumDeserializer converter = new JacksonEnumDeserializer(); converter.setClazz(rawCls); return converter; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/spring/SpringCacheConfig.java ================================================ package im.zhaojun.zfile.core.config.spring; import im.zhaojun.zfile.core.config.security.SaTokenDaoRedisJackson; import org.apache.commons.lang3.BooleanUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.concurrent.ConcurrentMapCacheManager; import org.springframework.cache.support.NoOpCacheManager; import org.springframework.cache.transaction.TransactionAwareCacheManagerProxy; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * Spring Cache 相关配置 * * @author zhaojun */ @Configuration @EnableCaching public class SpringCacheConfig { @Value("${zfile.dbCache.enable:true}") private Boolean dbCacheEnable; /** * 使用 TransactionAwareCacheManagerProxy 装饰 ConcurrentMapCacheManager,使其支持事务 (将 put、evict、clear 操作延迟到事务成功提交再执行.) */ @Bean @ConditionalOnMissingBean(SaTokenDaoRedisJackson.class) public CacheManager cacheManager() { return BooleanUtils.isNotTrue(dbCacheEnable) ? new NoOpCacheManager() : new TransactionAwareCacheManagerProxy(new ConcurrentMapCacheManager()); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/spring/StringToEnumConverterFactory.java ================================================ package im.zhaojun.zfile.core.config.spring; import com.baomidou.mybatisplus.annotation.EnumValue; import com.baomidou.mybatisplus.annotation.IEnum; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import im.zhaojun.zfile.core.exception.core.SystemException; import jakarta.validation.constraints.NotNull; import lombok.extern.slf4j.Slf4j; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; /** * String 转枚举通用转换器工厂 * * @author zhaojun */ @Slf4j public class StringToEnumConverterFactory implements ConverterFactory> { /** * 存储枚举类型的缓存 */ private static final Map, Converter>> CONVERTER_MAP = new ConcurrentHashMap<>(); /** * 枚举类的获取枚举值方法缓存 */ private static final Map, Method> TABLE_METHOD_OF_ENUM_TYPES = new ConcurrentHashMap<>(); @Override @SuppressWarnings("unchecked cast") public > Converter getConverter(Class targetType) { // 缓存转换器 Converter converter = (Converter) CONVERTER_MAP.get(targetType); if (converter == null) { converter = new StringToEnumConverter<>(targetType); CONVERTER_MAP.put(targetType, converter); } return converter; } static class StringToEnumConverter> implements Converter { private final Map enumMap = new ConcurrentHashMap<>(); StringToEnumConverter(Class enumType) { Method method = getMethod(enumType); T[] enums = enumType.getEnumConstants(); // 将值与枚举对象对应并缓存 for (T e : enums) { try { enumMap.put(method.invoke(e).toString(), e); } catch (IllegalAccessException | InvocationTargetException ex) { log.error("获取枚举值错误!!! ", ex); } } } @Override public T convert(@NotNull String source) { // 获取 T t = enumMap.get(source); if (t == null) { throw new SystemException("该字符串找不到对应的枚举对象 字符串:" + source); } return t; } } public static Method getMethod(Class enumType) { Method method; // 找到取值的方法 if (IEnum.class.isAssignableFrom(enumType)) { try { method = enumType.getMethod("getValue"); } catch (NoSuchMethodException e) { throw new SystemException(String.format("类:%s 找不到 getValue方法", enumType.getName())); } } else { method = TABLE_METHOD_OF_ENUM_TYPES.computeIfAbsent(enumType, k -> { Field field = dealEnumType(enumType).orElseThrow(() -> new IllegalArgumentException(String.format( "类:%s 找不到 EnumValue注解", enumType.getName()))); Class fieldType = field.getType(); String fieldName = field.getName(); String methodName = StringUtils.concatCapitalize(boolean.class.equals(fieldType) ? "is" : "get", fieldName); try { return enumType.getDeclaredMethod(methodName); } catch (NoSuchMethodException e) { e.printStackTrace(); } return null; }); } return method; } private static Optional dealEnumType(Class clazz) { return clazz.isEnum() ? Arrays.stream(clazz.getDeclaredFields()).filter(field -> field.isAnnotationPresent(EnumValue.class)).findFirst() : Optional.empty(); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/spring/WebMvcConfig.java ================================================ package im.zhaojun.zfile.core.config.spring; import im.zhaojun.zfile.module.storage.model.enums.StorageTypeEnum; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.servlet.server.ServletWebServerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.format.FormatterRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * ZFile Web 相关配置. * * @author zhaojun */ @Configuration public class WebMvcConfig implements WebMvcConfigurer { /** * 支持 url 中传入 <>[\]^`{|} 这些特殊字符. */ @Bean public ServletWebServerFactory webServerFactory() { TomcatServletWebServerFactory webServerFactory = new TomcatServletWebServerFactory(); // 添加对 URL 中特殊符号的支持. webServerFactory.addConnectorCustomizers(connector -> { connector.setProperty("relaxedPathChars", "<>[\\]^`{|}%[]"); connector.setProperty("relaxedQueryChars", "<>[\\]^`{|}%[]"); }); return webServerFactory; } /** * 添加自定义枚举格式化器. * @see StorageTypeEnum */ @Override public void addFormatters(FormatterRegistry registry) { registry.addConverterFactory(new StringToEnumConverterFactory()); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/totp/TotpAutoConfiguration.java ================================================ package im.zhaojun.zfile.core.config.totp; import dev.samstevens.totp.TotpInfo; import dev.samstevens.totp.code.*; import dev.samstevens.totp.qr.QrDataFactory; import dev.samstevens.totp.secret.DefaultSecretGenerator; import dev.samstevens.totp.secret.SecretGenerator; import dev.samstevens.totp.time.SystemTimeProvider; import dev.samstevens.totp.time.TimeProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @ConditionalOnClass({TotpInfo.class}) @EnableConfigurationProperties({TotpProperties.class}) public class TotpAutoConfiguration { private final TotpProperties props; @Autowired public TotpAutoConfiguration(TotpProperties props) { this.props = props; } @Bean @ConditionalOnMissingBean public SecretGenerator secretGenerator() { int length = this.props.getSecret().getLength(); return new DefaultSecretGenerator(length); } @Bean @ConditionalOnMissingBean public TimeProvider timeProvider() { return new SystemTimeProvider(); } @Bean @ConditionalOnMissingBean public HashingAlgorithm hashingAlgorithm() { return HashingAlgorithm.SHA1; } @Bean @ConditionalOnMissingBean public QrDataFactory qrDataFactory(HashingAlgorithm hashingAlgorithm) { return new QrDataFactory(hashingAlgorithm, this.getCodeLength(), this.getTimePeriod()); } @Bean @ConditionalOnMissingBean public CodeGenerator codeGenerator(HashingAlgorithm algorithm) { return new DefaultCodeGenerator(algorithm, this.getCodeLength()); } @Bean @ConditionalOnMissingBean public CodeVerifier codeVerifier(CodeGenerator codeGenerator, TimeProvider timeProvider) { DefaultCodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider); verifier.setTimePeriod(this.getTimePeriod()); verifier.setAllowedTimePeriodDiscrepancy(this.props.getTime().getDiscrepancy()); return verifier; } private int getCodeLength() { return this.props.getCode().getLength(); } private int getTimePeriod() { return this.props.getTime().getPeriod(); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/config/totp/TotpProperties.java ================================================ package im.zhaojun.zfile.core.config.totp; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties( prefix = "totp" ) public class TotpProperties { private static final int DEFAULT_SECRET_LENGTH = 32; private static final int DEFAULT_CODE_LENGTH = 6; private static final int DEFAULT_TIME_PERIOD = 30; private static final int DEFAULT_TIME_DISCREPANCY = 1; private final Secret secret = new Secret(); private final Code code = new Code(); private final Time time = new Time(); public TotpProperties() { } public Secret getSecret() { return this.secret; } public Code getCode() { return this.code; } public Time getTime() { return this.time; } public static class Time { private int period = 30; private int discrepancy = 1; public Time() { } public int getPeriod() { return this.period; } public void setPeriod(int period) { this.period = period; } public int getDiscrepancy() { return this.discrepancy; } public void setDiscrepancy(int discrepancy) { this.discrepancy = discrepancy; } } public static class Code { private int length = 6; public Code() { } public int getLength() { return this.length; } public void setLength(int length) { this.length = length; } } public static class Secret { private int length = 32; public Secret() { } public int getLength() { return this.length; } public void setLength(int length) { this.length = length; } } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/constant/MdcConstant.java ================================================ package im.zhaojun.zfile.core.constant; /** * Slf4j mdc 常量 * * @author zhaojun */ public class MdcConstant { public static final String TRACE_ID = "traceId"; public static final String IP = "ip"; public static final String USER = "user"; } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/constant/RuleTypeConstant.java ================================================ package im.zhaojun.zfile.core.constant; /** * 规则表达式类型常量 * * @author zhaojun */ public class RuleTypeConstant { public static final String IP = "ip"; public static final String REGEX = "regex"; public static final String ANT_PATH = "antPath"; public static final String SPRING_SIMPLE = "springSimple"; } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/constant/ZFileConstant.java ================================================ package im.zhaojun.zfile.core.constant; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; /** * ZFile 常量 * * @author zhaojun */ @Configuration public class ZFileConstant { /** * 最大支持文本文件大小为 ? KB 的文件内容. */ public static Long TEXT_MAX_FILE_SIZE_KB = 100L; @Autowired(required = false) public void setTextMaxFileSizeMb(@Value("${zfile.preview.text.maxFileSizeKb}") Long maxFileSizeKb) { ZFileConstant.TEXT_MAX_FILE_SIZE_KB = maxFileSizeKb; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/constant/ZFileHttpHeaderConstant.java ================================================ package im.zhaojun.zfile.core.constant; /** * ZFile 自定义 HTTP 请求头常量 * * @author zhaojun */ public class ZFileHttpHeaderConstant { public static final String ZFILE_TOKEN = "Zfile-Token"; public static final String AXIOS_REQUEST = "Axios-Request"; public static final String AXIOS_FROM = "Axios-From"; } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/controller/FrontIndexController.java ================================================ package im.zhaojun.zfile.core.controller; import im.zhaojun.zfile.core.util.StringUtils; import im.zhaojun.zfile.module.config.model.dto.SystemConfigDTO; import im.zhaojun.zfile.module.config.service.SystemConfigService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.web.WebProperties; import org.springframework.core.io.FileSystemResourceLoader; import org.springframework.core.io.ResourceLoader; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import java.nio.charset.StandardCharsets; /** * 处理前端首页 Controller * * @author zhaojun */ @Slf4j @Controller public class FrontIndexController { @Resource private SystemConfigService systemConfigService; @Resource private WebProperties webProperties; /** * 所有未找到的页面都跳转到首页, 用户解决 vue history 直接访问 404 的问题 * 同时, 读取 index.html 文件, 修改 title 和 favicon 后返回. * * @return 转发到 /index.html */ @RequestMapping(value = { "/"}) @ResponseBody public ResponseEntity redirect() { // 读取 resources/static/index.html 文件修改 title 和 favicon 后返回 ResourceLoader resourceLoader = new FileSystemResourceLoader(); String[] staticLocations = webProperties.getResources().getStaticLocations(); // 如果 staticLocations 里没有包含 file:static/, 则手动添加 boolean fileStaticExist = false; for (String staticLocation : staticLocations) { if (staticLocation.startsWith("file:")) { fileStaticExist = true; break; } } if (!fileStaticExist) { staticLocations = org.apache.commons.lang3.ArrayUtils.add(staticLocations, "file:static/"); } for (String staticLocation : staticLocations) { org.springframework.core.io.Resource resource = resourceLoader.getResource(staticLocation + "/index.html"); boolean exists = resource.exists(); if (exists) { String content; try { content = resource.getContentAsString(StandardCharsets.UTF_8); if (log.isTraceEnabled()) { log.trace("读取 index.html 文件成功, 文件路径: {}", staticLocation); } } catch (Exception e) { log.error("{} 资源存在但读取 index.html 文件失败.", staticLocation); return ResponseEntity.status(500).body("static index.html read error"); } SystemConfigDTO systemConfig = systemConfigService.getSystemConfig(); // 替换为系统设置中的站点名称 String siteName = systemConfig.getSiteName(); if (StringUtils.isNotBlank(siteName)) { content = content.replace("ZFile", "" + siteName + ""); } // 替换为系统设置中的 favicon 地址 String faviconUrl = systemConfig.getFaviconUrl(); if (StringUtils.isNotBlank(faviconUrl)) { content = content.replace("/favicon.svg", faviconUrl); } // 添加缓存控制头 return ResponseEntity.ok() .header("Cache-Control", "max-age=600, must-revalidate, proxy-revalidate") .header("Pragma", "no-cache") .body(content); } } return ResponseEntity.status(404).body("static index.html not found"); } @RequestMapping(value = { "/guest"}) @ResponseBody public String guest() { SystemConfigDTO systemConfig = systemConfigService.getSystemConfig(); return systemConfig.getGuestIndexHtml(); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/controller/LogController.java ================================================ package im.zhaojun.zfile.core.controller; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.ZipUtil; import com.github.xiaoymin.knife4j.annotations.ApiSort; import im.zhaojun.zfile.core.annotation.DemoDisable; import im.zhaojun.zfile.core.util.FileResponseUtil; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.io.File; import java.util.Date; /** * 获取系统日志接口 * * @author zhaojun */ @Tag(name = "日志") @ApiSort(8) @Slf4j @RestController @RequestMapping("/admin") public class LogController { @Value("${zfile.log.path}") private String zfileLogPath; @GetMapping("/log/download") @Operation(summary = "下载系统日志") @DemoDisable public ResponseEntity downloadLog() { if (log.isDebugEnabled()) { log.debug("下载诊断日志"); } File fileZip = ZipUtil.zip(zfileLogPath); String currentDate = DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"); return FileResponseUtil.exportSingleThread(fileZip, "ZFile 诊断日志 - " + currentDate + ".zip"); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/exception/ErrorCode.java ================================================ package im.zhaojun.zfile.core.exception; import lombok.Getter; /** * 异常信息枚举类 * * @author zhaojun */ @Getter public enum ErrorCode { /** * 系统异常 */ SYSTEM_ERROR("50000", "系统异常"), INVALID_STORAGE_SOURCE("50001", "无效或初始化失败的存储源"), DEMO_SITE_DISABLE_OPERATOR("50002", "演示站点不允许此操作"), /** * 业务异常 4xxxx. * 第二位为 0 时,是系统初始化相关错误 * 第二位为 1 时,是前台(文件管理)错误 * 第二位为 2 时,是登录错误 * 第二位为 3 时,是管理员端错误 */ BIZ_ERROR("40000", "操作失败"), BIZ_NOT_FOUND("40400", "NOT FOUND"), // 第二位为 0 时,是系统初始化相关错误 BIZ_SYSTEM_ALREADY_INIT("40001", "系统已初始化,请勿重复初始化"), BIZ_SYSTEM_INIT_ERROR("40002", "系统初始化错误"), // 第二位为 1 时,是前台(文件管理)错误 BIZ_BAD_REQUEST("41000", "请求参数异常"), BIZ_UNSUPPORTED_PROXY_DOWNLOAD("41001", "该存储源不支持代理下载"), BIZ_INVALID_SIGNATURE("41002", "签名无效或下载地址已过期"), BIZ_PREVIEW_FILE_SIZE_EXCEED("41003", "预览文本文件大小超出系统限制"), BIZ_FILE_NOT_EXIST("41004", "文件不存在"), BIZ_ACCESS_TOO_FREQUENT("41005", "请求太频繁了,请稍后再试"), BIZ_UPLOAD_FILE_NOT_EMPTY("41006", "上传文件不能为空"), BIZ_UPLOAD_FILE_ERROR("41010", "上传文件失败"), BIZ_UPLOAD_FILE_TIMEOUT_ERROR("41026", "上传文件超时"), BIZ_EXPIRE_TIME_ILLEGAL("41007", "过期时间不合法"), BIZ_DELETE_FILE_NOT_EMPTY("41008", "非空文件夹不允许删除"), BIZ_FILE_PATH_ILLEGAL("41009", "文件名/路径存在安全隐患"), BIZ_DIRECT_LINK_NOT_ALLOWED("41011", "当前系统不允许使用直链"), BIZ_SHORT_LINK_NOT_ALLOWED("41012", "当前系统不允许使用短链"), BIZ_SHORT_LINK_EXPIRED("41013", "短链已失效"), BIZ_SHORT_LINK_NOT_FOUNT("41014", "短链不存在"), BIZ_DIRECT_LINK_EXPIRED("41015", "直链已失效"), BIZ_STORAGE_NOT_SUPPORT_OPERATION("41016", "该存储类型不支持此操作"), BIZ_STORAGE_NOT_FOUND("41017", "存储源不存在"), BIZ_STORAGE_SOURCE_ILLEGAL_OPERATION("41018", "非法或未授权的操作"), BIZ_STORAGE_SOURCE_FILE_FORBIDDEN("41019", "文件目录无访问权限"), BIZ_STORAGE_SOURCE_FOLDER_PASSWORD_REQUIRED("41020", "此文件夹需要密码"), BIZ_STORAGE_SOURCE_FOLDER_PASSWORD_ERROR("41021", "密码错误"), BIZ_INVALID_FILE_NAME("41022", "文件名不合法"), BIZ_UNSUPPORTED_OPERATION("41023", "不支持的操作"), BIZ_FTP_CLIENT_POOL_FULL("41024", "FTP 客户端连接池已满"), BIZ_SFTP_CLIENT_POOL_FULL("41025", "SFTP 客户端连接池已满"), BIZ_FOLDER_NOT_EXIST("41026", "文件夹不存在"), BIZ_UPLOAD_FILE_TYPE_NOT_ALLOWED("41027", "不允许上传的文件"), BIZ_RENAME_FILE_TYPE_NOT_ALLOWED("41028", "不允许重命名到该名称"), BIZ_UNSUPPORTED_OPERATION_TYPE("41029", "不支持的操作类型"), BIZ_CUSTOM_SHARE_LINK_KEY_FORMAT_ILLEGAL("41030", "自定义分享 key 格式不正确,只能包含字母、数字、下划线和短横线,长度为 3-8 位"), BIZ_SHARE_LINK_KEY_ALREADY_EXIST("41031", "分享 key 已存在"), BIZ_SHARE_LINK_EXPIRY_MUST_BE_FUTURE("41032", "过期时间必须是未来的时间"), BIZ_SHARE_LINK_NOT_EXIST("41033", "分享链接不存在"), BIZ_SHARE_LINK_EXPIRED("41034", "分享链接已过期"), BIZ_SHARE_PASSWORD_ERROR("41036", "分享密码错误"), BIZ_SHARE_FILE_LIST_ERROR("41037", "获取分享文件列表失败"), BIZ_SHARE_FILE_DOWNLOAD_ERROR("41038", "获取文件下载地址失败"), BIZ_SHARE_FILE_INFO_ERROR("41039", "获取文件信息失败"), // 第二位为 2 时,是登录错误 BIZ_UNAUTHORIZED("42000", "未登录或未授权"), BIZ_LOGIN_ERROR("42001", "登录失败, 账号或密码错误"), BIZ_VERIFY_CODE_ERROR("42002", "验证码错误或已失效"), // 第二位为 3 时,是管理员端错误 BIZ_ADMIN_ERROR("43000", "操作失败"), BIZ_USER_NOT_EXIST("43001", "用户不存在"), BIZ_USER_EXIST("43002", "用户已存在"), BIZ_PASSWORD_NOT_SAME("43003", "两次密码不一致"), BIZ_OLD_PASSWORD_ERROR("43004", "旧密码不匹配"), BIZ_DELETE_BUILT_IN_USER("43005", "不能删除内置用户"), BIZ_UNSUPPORTED_STORAGE_TYPE("43006", "不支持的存储类型"), BIZ_STORAGE_KEY_EXIST("43007", "存储源别名已存在"), BIZ_AUTO_GET_SHARE_POINT_SITES_ERROR("43008", "自动获取 SharePoint 网站列表失败"), BIZ_ORIGINS_NOT_EMPTY("43009", "请先在 \"站点设置\" 中配置站点域名"), BIZ_2FA_CODE_ERROR("43010", "双因素认证验证失败"), BIZ_STORAGE_INIT_ERROR("43011", "存储源初始化失败"), BIZ_RULE_EXIST("43012", "规则已存在"), BIZ_SSO_PROVIDER_EXIST("43013", "单点登录配置已存在"), BIZ_SSO_PROVIDER_DISABLED("43014", "此单点登录未启用"), /** * 通用的无权限异常 */ NO_FORBIDDEN("30000", "没有权限"), NO_CUSTOM_SHARE_LINK_KEY_PERMISSION("30001", "没有自定义分享链接 key 的权限"), /** * 授权校验异常 */ PRO_AUTH_CODE_EMPTY("20000", "请先去后台 \"基本设置\" 填写 \"授权码\""), PRO_CHECK_REFERER_EMPTY("20001", "Referer 无效,请检查服务端设置,20001"), // Referer 无效,请检查服务端设置 PRO_CHECK_TIME_NO_SYNC("20002", "授权校验失败, 服务器时间异常,20002"), // 授权校验失败, 服务器时间异常. PRO_AUTH_CODE_INVALID_ERROR("20003", "授权码无效, 请检查后台 \"站点设置\" 中的 \"授权码\" 20003"), PRO_CHECK_UNKNOWN_ERROR("20004", "授权验证异常,未知异常,20098"), PRO_MSG_ERROR("20005", null); private String code; private String message; ErrorCode(String code, String message) { this.code = code; this.message = message; } /** * 设置错误码 * * @param code 错误码 * @return 返回当前枚举 */ public ErrorCode setCode(String code) { this.code = code; return this; } /** * 设置错误信息 * * @param message 错误信息 * @return 返回当前枚举 */ public ErrorCode setMessage(String message) { this.message = message; return this; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/exception/GlobalExceptionHandler.java ================================================ package im.zhaojun.zfile.core.exception; import cn.dev33.satoken.exception.NotLoginException; import cn.dev33.satoken.exception.NotRoleException; import im.zhaojun.zfile.core.controller.FrontIndexController; import im.zhaojun.zfile.core.exception.biz.*; import im.zhaojun.zfile.core.exception.core.BizException; import im.zhaojun.zfile.core.exception.core.ErrorPageBizException; import im.zhaojun.zfile.core.exception.core.SystemException; import im.zhaojun.zfile.core.exception.status.*; import im.zhaojun.zfile.core.exception.system.UploadFileFailSystemException; import im.zhaojun.zfile.core.exception.system.ZFileAuthorizationSystemException; import im.zhaojun.zfile.core.util.AjaxJson; import im.zhaojun.zfile.core.util.RequestHolder; import im.zhaojun.zfile.core.util.StringUtils; import im.zhaojun.zfile.module.config.service.SystemConfigService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.catalina.connector.ClientAbortException; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.servlet.resource.NoResourceFoundException; import org.sqlite.SQLiteException; import java.io.FileNotFoundException; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Optional; /** * 全局异常处理 * * @author zhaojun */ @ControllerAdvice @Slf4j @Order(1) public class GlobalExceptionHandler { private static final ThreadLocal exceptionMessage = new ThreadLocal<>(); @Resource private SystemConfigService systemConfigService; @Resource private FrontIndexController frontIndexController; private static final int MAX_FIND_CAUSE_EXCEPTION_DEPTH = 10; // ---------------------- status exception start ---------------------- @ExceptionHandler(value = UnauthorizedAccessException.class) @ResponseBody @ResponseStatus(HttpStatus.UNAUTHORIZED) public AjaxJson unauthorizedAccessException() { if (RequestHolder.isAxiosRequest()) { return AjaxJson.getUnauthorizedResult(); } try { String unauthorizedUrl = systemConfigService.getUnauthorizedUrl(); RequestHolder.getResponse().sendRedirect(unauthorizedUrl); } catch (IOException ex) { return AjaxJson.getUnauthorizedResult(); } return null; } @ExceptionHandler(value = { NotRoleException.class }) @ResponseBody @ResponseStatus(HttpStatus.FORBIDDEN) public AjaxJson forbiddenAccessException() { if (RequestHolder.isAxiosRequest()) { return AjaxJson.getForbiddenResult(); } try { String forbiddenUrl = systemConfigService.getForbiddenUrl(); RequestHolder.getResponse().sendRedirect(forbiddenUrl); } catch (IOException ex) { return AjaxJson.getForbiddenResult(); } return null; } @ExceptionHandler(value = ForbiddenAccessException.class) @ResponseBody @ResponseStatus(HttpStatus.FORBIDDEN) public AjaxJson forbiddenAccessException(ForbiddenAccessException e) { if (RequestHolder.isAxiosRequest()) { return AjaxJson.getError(e.getCode(), e.getMessage()); } try { String forbiddenUrl = systemConfigService.getForbiddenUrl(e.getCode(), e.getMessage()); RequestHolder.getResponse().sendRedirect(forbiddenUrl); } catch (IOException ex) { return AjaxJson.getError(e.getCode(), e.getMessage()); } return null; } @ExceptionHandler(value = NotFoundAccessException.class) @ResponseBody @ResponseStatus(HttpStatus.NOT_FOUND) public AjaxJson notFoundAccessException(NotFoundAccessException e) { if (RequestHolder.isAxiosRequest()) { return AjaxJson.getError(e.getCode(), e.getMessage()); } try { String notFoundUrl = systemConfigService.getNotFoundUrl(e.getCode(), e.getMessage()); RequestHolder.getResponse().sendRedirect(notFoundUrl); } catch (IOException ex) { return AjaxJson.getError(e.getCode(), e.getMessage()); } return null; } /** * 所有未找到的页面都跳转到首页, 用户解决 vue history 直接访问 404 的问题 * 同时, 读取 index.html 文件, 修改 title 和 favicon 后返回. * * @return 转发到 /index.html */ @ExceptionHandler(value = NoResourceFoundException.class) @ResponseBody public String notFoundAccessException() { return frontIndexController.redirect().getBody(); } @ExceptionHandler(value = MethodNotAllowedAccessException.class) @ResponseBody @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) public AjaxJson methodNotAllowedAccessException(MethodNotAllowedAccessException e) { return new AjaxJson<>(e.getCode(), e.getMessage()); } @ExceptionHandler(value = BadRequestAccessException.class) @ResponseBody @ResponseStatus(HttpStatus.BAD_REQUEST) public AjaxJson badRequestAccessException(BadRequestAccessException e) { return new AjaxJson<>(e.getCode(), e.getMessage()); } // ---------------------- status exception end ---------------------- // ---------------------- biz exception start ---------------------- @ExceptionHandler(value = APIHttpRequestBizException.class) @ResponseBody @ResponseStatus public AjaxJson apiHttpRequestBizException(APIHttpRequestBizException e) { log.warn("请求第三方 API 异常, 请求地址: {}, 响应码: {}, 响应体: {}", e.getUrl(), e.getResponseCode(), e.getResponseBody()); return new AjaxJson<>(e.getCode(), e.getMessage()); } @ExceptionHandler(value = FilePathSecurityBizException.class) @ResponseBody @ResponseStatus(HttpStatus.BAD_REQUEST) public AjaxJson filePathSecurityBizException(FilePathSecurityBizException e) { log.warn("获取文件路径存在安全风险, 文件路径: {}", e.getPath()); return new AjaxJson<>(e.getCode(), e.getMessage()); } @ExceptionHandler(value = GetPreviewTextContentBizException.class) @ResponseBody @ResponseStatus public AjaxJson getPreviewTextContentBizException(GetPreviewTextContentBizException e) { log.warn("获取预览文件内容失败, 文件 url: {}", e.getUrl(), e); return new AjaxJson<>(e.getCode(), "预览文件内容失败, 请联系管理员."); } @ExceptionHandler(value = InitializeStorageSourceBizException.class) @ResponseBody @ResponseStatus public AjaxJson initializeStorageSourceBizException(InitializeStorageSourceBizException e) { log.error("存储源初始化失败, 存储源 ID: {}.", e.getStorageId(), e); return new AjaxJson<>(e.getCode(), "存储源初始化失败:" + e.getMessage()); } @ExceptionHandler(value = StorageSourceFileForbiddenAccessBizException.class) @ResponseBody @ResponseStatus public AjaxJson storageSourceFileForbiddenAccessBizException(StorageSourceFileForbiddenAccessBizException e) { log.warn("尝试访问不被授权的文件/目录, 存储源 ID: {}: 目录: {}", e.getStorageId(), e.getPath()); return new AjaxJson<>(e.getCode(), e.getMessage()); } @ExceptionHandler(value = StorageSourceIllegalOperationBizException.class) @ResponseBody @ResponseStatus public AjaxJson storageSourceIllegalOperationBizException(StorageSourceIllegalOperationBizException e) { log.warn("存储源非法或未授权的操作, 存储源 ID: {}, 操作类型: {}", e.getStorageId(), e.getAction()); return new AjaxJson<>(e.getCode(), e.getMessage()); } @ExceptionHandler(value = CorsBizException.class) @ResponseBody @ResponseStatus public AjaxJson corsBizException(CorsBizException e) { log.warn("跨域异常:", e); return new AjaxJson<>(e.getCode(), e.getMessage()); } @ExceptionHandler(value = ErrorPageBizException.class) @ResponseBody @ResponseStatus public AjaxJson errorPageBizException(ErrorPageBizException e) { if (RequestHolder.isAxiosRequest()) { return AjaxJson.getError(e.getCode(), e.getMessage()); } try { String errorPageUrl = systemConfigService.getErrorPageUrl(e.getCode(), e.getMessage()); RequestHolder.getResponse().sendRedirect(errorPageUrl); } catch (IOException ex) { return AjaxJson.getError(e.getCode(), e.getMessage()); } return null; } @ExceptionHandler(value = BizException.class) @ResponseBody @ResponseStatus public AjaxJson bizException(BizException e) { return new AjaxJson<>(e.getCode(), e.getMessage()); } // ---------------------- biz exception end ---------------------- // ---------------------- system exception end ---------------------- @ExceptionHandler(value = UploadFileFailSystemException.class) @ResponseBody @ResponseStatus public AjaxJson uploadFileFailSystemException(UploadFileFailSystemException e) { log.warn("上传文件失败, 存储类型: {}, 上传路径: {}, 输入流可用字节数: {}, 响应码: {}, 响应体: {}", e.getStorageTypeEnum(), e.getUploadPath(), e.getInputStreamAvailable(), e.getResponseCode(), e.getResponseBody()); return new AjaxJson<>(e.getCode(), e.getMessage()); } @ExceptionHandler(value = ZFileAuthorizationSystemException.class) @ResponseBody @ResponseStatus public AjaxJson zfileAuthorizationSystemException(ZFileAuthorizationSystemException e) { return new AjaxJson<>(e.getCode(), e.getMessage()); } @ExceptionHandler(value = SystemException.class) @ResponseBody @ResponseStatus public AjaxJson systemException(SystemException e) { return new AjaxJson<>(e.getCode(), e.getMessage()); } // ---------------------- system exception end ---------------------- // ---------------------- common exception end ---------------------- @ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class}) @ResponseBody @ResponseStatus(code = HttpStatus.BAD_REQUEST) public AjaxJson> handleValidException(Exception e) { BindingResult bindingResult = null; if (e instanceof MethodArgumentNotValidException) { bindingResult = ((MethodArgumentNotValidException) e).getBindingResult(); } else if (e instanceof BindException) { bindingResult = ((BindException) e).getBindingResult(); } Map errorMap = new HashMap<>(16); Optional.ofNullable(bindingResult) .map(BindingResult::getFieldErrors) .ifPresent(fieldErrors -> { for (FieldError fieldError : fieldErrors) { errorMap.put(fieldError.getField(), fieldError.getDefaultMessage()); } }); return new AjaxJson<>(ErrorCode.BIZ_BAD_REQUEST.getCode(), ErrorCode.BIZ_BAD_REQUEST.getMessage(), errorMap); } @ExceptionHandler({FileNotFoundException.class}) @ResponseBody @ResponseStatus(HttpStatus.NOT_FOUND) public AjaxJson fileNotFound() { return AjaxJson.getError("文件不存在"); } /** * 登录异常拦截器 */ @ExceptionHandler(NotLoginException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) @ResponseBody public AjaxJson handlerNotLoginException(NotLoginException e) { if (RequestHolder.isAxiosRequest()) { return AjaxJson.getUnauthorizedResult(); } try { String domain = systemConfigService.getRealFrontDomain(); if (StringUtils.isBlank(domain)) { domain = ""; } String loginUrl = StringUtils.concat(domain, "/login"); RequestHolder.getResponse().sendRedirect(loginUrl); } catch (IOException ex) { return AjaxJson.getUnauthorizedResult(); } return null; } @ExceptionHandler @ResponseBody @ResponseStatus public AjaxJson extraExceptionHandler(Exception e) { ExceptionType exceptionType = getExceptionType(e); if (exceptionType == ExceptionType.IGNORE_PRINT_STACK_TRACE_EXCEPTION) { log.warn(e.getMessage()); } else if (exceptionType == ExceptionType.OTHER) { log.error(e.getMessage(), e); } else if (exceptionType == ExceptionType.SPECIFY_MESSAGE_EXCEPTION) { if (exceptionMessage.get() != null) { String message = exceptionMessage.get(); log.error("发生异常: {}", message,e ); exceptionMessage.remove(); return AjaxJson.getError(message); } } else if (exceptionType == ExceptionType.IGNORE_EXCEPTION) { // 忽略异常 return null; } if (e.getClass() == Exception.class) { return AjaxJson.getError("系统异常, 请联系管理员"); } else { return AjaxJson.getError(e.getMessage()); } } private static ExceptionType getExceptionType(Exception e) { int findCauseCount = 0; do { if (e instanceof BizException) { return ExceptionType.IGNORE_PRINT_STACK_TRACE_EXCEPTION; } else if (e instanceof ClientAbortException) { return ExceptionType.IGNORE_EXCEPTION; } else if (e instanceof SQLiteException && e.getMessage().contains("database is locked")) { exceptionMessage.set("数据库繁忙,请稍后再试"); return ExceptionType.SPECIFY_MESSAGE_EXCEPTION; } e = (Exception) e.getCause(); findCauseCount++; } while (e != null && findCauseCount < MAX_FIND_CAUSE_EXCEPTION_DEPTH); return ExceptionType.OTHER; } enum ExceptionType { /** * 忽略打印异常信息和堆栈信息 */ IGNORE_EXCEPTION, /** * 仅打印异常信息, 不打印堆栈信息 */ IGNORE_PRINT_STACK_TRACE_EXCEPTION, /** * 不打印堆栈信息,但指定异常信息 */ SPECIFY_MESSAGE_EXCEPTION, /** * 其他异常, 打印异常信息和堆栈信息 */ OTHER; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/exception/biz/APIHttpRequestBizException.java ================================================ package im.zhaojun.zfile.core.exception.biz; import im.zhaojun.zfile.core.exception.ErrorCode; import im.zhaojun.zfile.core.exception.core.BizException; import im.zhaojun.zfile.core.exception.GlobalExceptionHandler; import lombok.Getter; /** * 请求第三方 API 时如果返回非 2xx 状态码, 则抛出此异常. 需记录请求地址, 响应状态码, 响应内容. *

* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#apiHttpRequestBizException(APIHttpRequestBizException)} * * @author zhaojun */ @Getter public class APIHttpRequestBizException extends BizException { private final String url; private final int responseCode; private final String responseBody; public APIHttpRequestBizException(ErrorCode errorCode, String url, int responseCode, String responseBody) { super(errorCode); this.url = url; this.responseCode = responseCode; this.responseBody = responseBody; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/exception/biz/CorsBizException.java ================================================ package im.zhaojun.zfile.core.exception.biz; import im.zhaojun.zfile.core.exception.core.BizException; import lombok.Getter; /** * @author zhaojun */ @Getter public class CorsBizException extends BizException { public CorsBizException(String message, Throwable cause) { super(message, cause); } @Override public boolean printExceptionStackTrace() { return true; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/exception/biz/FilePathSecurityBizException.java ================================================ package im.zhaojun.zfile.core.exception.biz; import im.zhaojun.zfile.core.exception.ErrorCode; import im.zhaojun.zfile.core.exception.core.BizException; import im.zhaojun.zfile.core.exception.GlobalExceptionHandler; import lombok.Getter; /** * 文件路径安全异常, 表示文件路径不合法,如包含了 "./" 或 "../" 等字符来尝试访问非法目录. *

* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#filePathSecurityBizException(FilePathSecurityBizException)} * * @author zhaojun */ @Getter public class FilePathSecurityBizException extends BizException { private final String path; public FilePathSecurityBizException(String path) { super(ErrorCode.BIZ_FILE_PATH_ILLEGAL); this.path = path; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/exception/biz/GetPreviewTextContentBizException.java ================================================ package im.zhaojun.zfile.core.exception.biz; import im.zhaojun.zfile.core.exception.core.BizException; import im.zhaojun.zfile.core.exception.GlobalExceptionHandler; import lombok.Getter; /** * 获取预览文件内容异常, 可能是目标连接无法访问/文件不存在等原因. *

* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#getPreviewTextContentBizException(GetPreviewTextContentBizException)} * * @author zhaojun */ @Getter public class GetPreviewTextContentBizException extends BizException { /** * 获取预览文件的 URL */ private final String url; public GetPreviewTextContentBizException(String url, Throwable cause) { super(cause); this.url = url; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/exception/biz/InitializeStorageSourceBizException.java ================================================ package im.zhaojun.zfile.core.exception.biz; import im.zhaojun.zfile.core.exception.GlobalExceptionHandler; import im.zhaojun.zfile.core.exception.core.BizException; import lombok.Getter; /** * 初始化存储源时失败产生的异常 *

* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#initializeStorageSourceBizException(InitializeStorageSourceBizException)} * * @author zhaojun */ @Getter public class InitializeStorageSourceBizException extends BizException { private final Integer storageId; public InitializeStorageSourceBizException(String message, Integer storageId) { super(message); this.storageId = storageId; } public InitializeStorageSourceBizException(String code, String message, Integer storageId, Throwable cause) { super(code, message, cause); this.storageId = storageId; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/exception/biz/InvalidStorageSourceBizException.java ================================================ package im.zhaojun.zfile.core.exception.biz; import im.zhaojun.zfile.core.exception.ErrorCode; import im.zhaojun.zfile.core.exception.core.BizException; import lombok.Getter; /** * 不存在或初始化失败的存储源异常。 * * @author zhaojun */ @Getter public class InvalidStorageSourceBizException extends BizException { private final Integer storageId; private final String storageKey; public InvalidStorageSourceBizException(String storageKey) { super(ErrorCode.INVALID_STORAGE_SOURCE); this.storageKey = storageKey; this.storageId = null; } public InvalidStorageSourceBizException(Integer storageId) { super(ErrorCode.INVALID_STORAGE_SOURCE); this.storageId = storageId; this.storageKey = null; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/exception/biz/StorageSourceFileForbiddenAccessBizException.java ================================================ package im.zhaojun.zfile.core.exception.biz; import im.zhaojun.zfile.core.exception.ErrorCode; import im.zhaojun.zfile.core.exception.core.BizException; import im.zhaojun.zfile.core.exception.GlobalExceptionHandler; import lombok.Getter; /** * 访问了禁止访问的存储源文件/目录时抛出此异常. *

* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#storageSourceFileForbiddenAccessBizException(StorageSourceFileForbiddenAccessBizException)} * * @author zhaojun */ @Getter public class StorageSourceFileForbiddenAccessBizException extends BizException { private final Integer storageId; private final String path; public StorageSourceFileForbiddenAccessBizException(Integer storageId, String path) { super(ErrorCode.BIZ_STORAGE_SOURCE_FILE_FORBIDDEN); this.storageId = storageId; this.path = path; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/exception/biz/StorageSourceIllegalOperationBizException.java ================================================ package im.zhaojun.zfile.core.exception.biz; import im.zhaojun.zfile.core.exception.ErrorCode; import im.zhaojun.zfile.core.exception.core.BizException; import im.zhaojun.zfile.core.exception.GlobalExceptionHandler; import im.zhaojun.zfile.module.storage.model.enums.FileOperatorTypeEnum; import lombok.Getter; /** * 对存储源进行非法(未授权)的操作产生的异常 *

* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#storageSourceIllegalOperationBizException(StorageSourceIllegalOperationBizException)} * * @author zhaojun */ @Getter public class StorageSourceIllegalOperationBizException extends BizException { private final Integer storageId; private final FileOperatorTypeEnum action; public StorageSourceIllegalOperationBizException(Integer storageId, FileOperatorTypeEnum action) { super(ErrorCode.BIZ_STORAGE_SOURCE_ILLEGAL_OPERATION); this.storageId = storageId; this.action = action; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/exception/core/BizException.java ================================================ package im.zhaojun.zfile.core.exception.core; import im.zhaojun.zfile.core.exception.ErrorCode; import lombok.Getter; /** * 业务异常,该类异常用户可自行处理,无需记录日志,属于正常业务流程中的异常. 如: 用户名密码错误, 未登录等. * * @author zhaojun */ @Getter public class BizException extends RuntimeException { private static final long serialVersionUID = 8312907182931723379L; /** * 错误码 */ private String code; /** * 是否打印堆栈信息,业务异常默认不打印堆栈信息,如果需要打印堆栈信息,可以通过子类覆盖该方法修改返回值为 true. */ public boolean printExceptionStackTrace() { return false; } /** * 构造一个没有错误信息的 SystemException */ public BizException() { super(); } /** * 使用指定的 Throwable 和 Throwable.toString() 作为异常信息来构造 SystemException * * @param cause 错误原因, 通过 Throwable.getCause() 方法可以获取传入的 cause信息 */ public BizException(Throwable cause) { super(cause); } /** * 使用错误信息 message 构造 SystemException * * @param message 错误信息 */ public BizException(String message) { super(message); } /** * 使用错误码和错误信息构造 SystemException * * @param code 错误码 * @param message 错误信息 */ public BizException(String code, String message) { super(message); this.code = code; } /** * 使用错误信息和 Throwable 构造 SystemException * * @param message 错误信息 * @param cause 错误原因 */ public BizException(String message, Throwable cause) { super(message, cause); } /** * @param code 错误码 * @param message 错误信息 * @param cause 错误原因 */ public BizException(String code, String message, Throwable cause) { super(message, cause); this.code = code; } /** * @param errorCode ErrorCode */ public BizException(ErrorCode errorCode) { super(errorCode.getMessage()); this.code = errorCode.getCode(); } /** * @param errorCode ErrorCode * @param cause 错误原因 */ public BizException(ErrorCode errorCode, Throwable cause) { super(errorCode.getMessage(), cause); this.code = errorCode.getCode(); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/exception/core/ErrorPageBizException.java ================================================ package im.zhaojun.zfile.core.exception.core; import im.zhaojun.zfile.core.exception.ErrorCode; import lombok.Getter; /** * 业务异常,该类异常用户可自行处理,无需记录日志,属于正常业务流程中的异常. 如: 用户名密码错误, 未登录等.
* 使用该类的异常,当该异常被抛出时,会跳转到 500 错误页面(错误码和错误消息可被 {@link #code} 和 {@link #getMessage()} 覆盖),而不是返回 JSON 数据.
* 一般使用该异常得请求不会是 AJAX 请求,而是直接在浏览器中访问的页面请求. * * @author zhaojun */ @Getter public class ErrorPageBizException extends RuntimeException { private static final long serialVersionUID = 8312907182931723379L; /** * 错误码 */ private String code; /** * 是否打印堆栈信息,业务异常默认不打印堆栈信息,如果需要打印堆栈信息,可以通过子类覆盖该方法修改返回值为 true. */ public boolean printExceptionStackTrace() { return false; } /** * 构造一个没有错误信息的 SystemException */ public ErrorPageBizException() { super(); } /** * 使用指定的 Throwable 和 Throwable.toString() 作为异常信息来构造 SystemException * * @param cause 错误原因, 通过 Throwable.getCause() 方法可以获取传入的 cause信息 */ public ErrorPageBizException(Throwable cause) { super(cause); } /** * 使用错误信息 message 构造 SystemException * * @param message 错误信息 */ public ErrorPageBizException(String message) { super(message); } /** * 使用错误码和错误信息构造 SystemException * * @param code 错误码 * @param message 错误信息 */ public ErrorPageBizException(String code, String message) { super(message); this.code = code; } /** * 使用错误信息和 Throwable 构造 SystemException * * @param message 错误信息 * @param cause 错误原因 */ public ErrorPageBizException(String message, Throwable cause) { super(message, cause); } /** * @param code 错误码 * @param message 错误信息 * @param cause 错误原因 */ public ErrorPageBizException(String code, String message, Throwable cause) { super(message, cause); this.code = code; } /** * @param errorCode ErrorCode */ public ErrorPageBizException(ErrorCode errorCode) { super(errorCode.getMessage()); this.code = errorCode.getCode(); } /** * @param errorCode ErrorCode * @param cause 错误原因 */ public ErrorPageBizException(ErrorCode errorCode, Throwable cause) { super(errorCode.getMessage(), cause); this.code = errorCode.getCode(); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/exception/core/SystemException.java ================================================ package im.zhaojun.zfile.core.exception.core; import im.zhaojun.zfile.core.exception.ErrorCode; import lombok.Getter; /** * 系统异常, 该类异常用户无法处理,需要记录日志, 属于系统异常. 如: 网络异常, 服务器异常等. * * @author zhaojun */ @Getter public class SystemException extends RuntimeException { private static final long serialVersionUID = 8312907182931723379L; /** * 错误码 */ private String code; /** * 构造一个没有错误信息的 SystemException */ public SystemException() { super(); } /** * 使用指定的 Throwable 和 Throwable.toString() 作为异常信息来构造 SystemException * * @param cause 错误原因, 通过 Throwable.getCause() 方法可以获取传入的 cause信息 */ public SystemException(Throwable cause) { super(cause); } /** * 使用错误信息 message 构造 SystemException * * @param message 错误信息 */ public SystemException(String message) { super(message); } /** * 使用错误码和错误信息构造 SystemException * * @param code 错误码 * @param message 错误信息 */ public SystemException(String code, String message) { super(message); this.code = code; } /** * 使用错误信息和 Throwable 构造 SystemException * * @param message 错误信息 * @param cause 错误原因 */ public SystemException(String message, Throwable cause) { super(message, cause); } /** * @param code 错误码 * @param message 错误信息 * @param cause 错误原因 */ public SystemException(String code, String message, Throwable cause) { super(message, cause); this.code = code; } /** * @param errorCode ErrorCode */ public SystemException(ErrorCode errorCode) { super(errorCode.getMessage()); this.code = errorCode.getCode(); } /** * @param errorCode ErrorCode * @param cause 错误原因 */ public SystemException(ErrorCode errorCode, Throwable cause) { super(errorCode.getMessage(), cause); this.code = errorCode.getCode(); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/exception/status/BadRequestAccessException.java ================================================ package im.zhaojun.zfile.core.exception.status; import im.zhaojun.zfile.core.exception.GlobalExceptionHandler; import im.zhaojun.zfile.core.exception.core.BizException; /** * 错误请求异常, 表示请求参数有误或者服务器无法理解, 一般返回 400 状态码 *

* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#badRequestAccessException(BadRequestAccessException)} * * @author zhaojun */ public class BadRequestAccessException extends BizException { public BadRequestAccessException(String message) { super(message); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/exception/status/ForbiddenAccessException.java ================================================ package im.zhaojun.zfile.core.exception.status; import im.zhaojun.zfile.core.exception.ErrorCode; import im.zhaojun.zfile.core.exception.core.BizException; /** * 禁止访问异常, 表示用户没有权限访问该资源, 一般返回 403 状态码. (已经有身份,如果没有身份,应该是 UnauthorizedAccessException) *

* 需要全局异常处理器捕获此异常, 并记录日志. {@link im.zhaojun.zfile.core.exception.GlobalExceptionHandler#forbiddenAccessException} * * @author zhaojun */ public class ForbiddenAccessException extends BizException { public ForbiddenAccessException(ErrorCode errorCode) { super(errorCode); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/exception/status/MethodNotAllowedAccessException.java ================================================ package im.zhaojun.zfile.core.exception.status; import im.zhaojun.zfile.core.exception.ErrorCode; import im.zhaojun.zfile.core.exception.GlobalExceptionHandler; import im.zhaojun.zfile.core.exception.core.BizException; /** * 错误请求异常, 表示请求方法有误或者服务器无法理解, 一般返回 405 状态码 *

* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#methodNotAllowedAccessException(MethodNotAllowedAccessException)} * * @author zhaojun */ public class MethodNotAllowedAccessException extends BizException { public MethodNotAllowedAccessException(ErrorCode errorCode) { super(errorCode); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/exception/status/NotFoundAccessException.java ================================================ package im.zhaojun.zfile.core.exception.status; import im.zhaojun.zfile.core.exception.ErrorCode; import im.zhaojun.zfile.core.exception.core.BizException; /** * 访问内容不存在异常, 表示用户请求的资源不存在时抛出, 一般返回 404 状态码. *

* 需要全局异常处理器捕获此异常, 并记录日志. {@link im.zhaojun.zfile.core.exception.GlobalExceptionHandler#notFoundAccessException} * * @author zhaojun */ public class NotFoundAccessException extends BizException { public NotFoundAccessException(ErrorCode errorCode) { super(errorCode); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/exception/status/UnauthorizedAccessException.java ================================================ package im.zhaojun.zfile.core.exception.status; import im.zhaojun.zfile.core.exception.core.BizException; /** * 禁止访问异常, 表示用户未进行身份认证, 一般返回 401 状态码. *

* 需要全局异常处理器捕获此异常, 并记录日志. {@link im.zhaojun.zfile.core.exception.GlobalExceptionHandler#unauthorizedAccessException} * * @author zhaojun */ public class UnauthorizedAccessException extends BizException { public UnauthorizedAccessException(String message) { super(message); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/exception/system/UploadFileFailSystemException.java ================================================ package im.zhaojun.zfile.core.exception.system; import im.zhaojun.zfile.core.exception.ErrorCode; import im.zhaojun.zfile.core.exception.GlobalExceptionHandler; import im.zhaojun.zfile.core.exception.core.SystemException; import im.zhaojun.zfile.module.storage.model.enums.StorageTypeEnum; import lombok.Getter; /** * 上传文件失败系统异常, 该异常用户无法处理,需要记录日志, 属于系统异常. 如: 网络异常, 目标存储源异常等 *

* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#uploadFileFailSystemException(UploadFileFailSystemException)} * * @author zhaojun */ @Getter public class UploadFileFailSystemException extends SystemException { private final StorageTypeEnum storageTypeEnum; private final String uploadPath; private final Long inputStreamAvailable; private final int responseCode; private final String responseBody; public UploadFileFailSystemException(StorageTypeEnum storageTypeEnum, String uploadPath, Long inputStreamAvailable, int responseCode, String responseBody) { this(storageTypeEnum, uploadPath, inputStreamAvailable, responseCode, responseBody, null); } public UploadFileFailSystemException(StorageTypeEnum storageTypeEnum, String uploadPath, Long inputStreamAvailable, int responseCode, String responseBody, Throwable cause) { super(ErrorCode.BIZ_UPLOAD_FILE_ERROR, cause); this.storageTypeEnum = storageTypeEnum; this.uploadPath = uploadPath; this.inputStreamAvailable = inputStreamAvailable; this.responseCode = responseCode; this.responseBody = responseBody; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/exception/system/ZFileAuthorizationSystemException.java ================================================ package im.zhaojun.zfile.core.exception.system; import im.zhaojun.zfile.core.exception.ErrorCode; import im.zhaojun.zfile.core.exception.GlobalExceptionHandler; import im.zhaojun.zfile.core.exception.core.SystemException; /** * ZFile 授权异常 *

* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#zfileAuthorizationSystemException(ZFileAuthorizationSystemException)} * * @author zhaojun */ public class ZFileAuthorizationSystemException extends SystemException { public ZFileAuthorizationSystemException(String code, String message) { super(code, message); } public ZFileAuthorizationSystemException(ErrorCode errorCode) { super(errorCode); } public ZFileAuthorizationSystemException(ErrorCode errorCode, Throwable cause) { super(errorCode, cause); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/filter/CorsFilter.java ================================================ package im.zhaojun.zfile.core.filter; import cn.hutool.core.util.ObjectUtil; import im.zhaojun.zfile.core.constant.ZFileHttpHeaderConstant; import im.zhaojun.zfile.core.util.StringUtils; import jakarta.servlet.*; import jakarta.servlet.annotation.WebFilter; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.core.annotation.Order; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; import org.springframework.web.cors.CorsUtils; import java.io.IOException; import java.util.Arrays; import java.util.List; /** * 开启跨域支持. 一般用于开发环境, 或前后端分离部署时开启. * * @author zhaojun */ @WebFilter(urlPatterns = "/*") @Order(Integer.MIN_VALUE) @Component public class CorsFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; if (httpServletRequest.getRequestURI().equals("/favicon.ico")) { return; } String header = httpServletRequest.getHeader(HttpHeaders.ORIGIN); List allowHeaders = Arrays.asList("Origin", "X-Requested-With", "Content-Type", "Accept", ZFileHttpHeaderConstant.ZFILE_TOKEN, ZFileHttpHeaderConstant.AXIOS_REQUEST, ZFileHttpHeaderConstant.AXIOS_FROM); httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, ObjectUtil.defaultIfNull(header, "*")); httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, StringUtils.join(",", allowHeaders)); httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS"); httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "false"); httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "600"); if (!CorsUtils.isPreFlightRequest(httpServletRequest)) { chain.doFilter(httpServletRequest, httpServletResponse); } } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/filter/MDCFilter.java ================================================ package im.zhaojun.zfile.core.filter; import cn.hutool.core.util.IdUtil; import cn.hutool.extra.servlet.JakartaServletUtil; import im.zhaojun.zfile.core.constant.MdcConstant; import im.zhaojun.zfile.core.util.ZFileAuthUtil; import jakarta.servlet.*; import jakarta.servlet.annotation.WebFilter; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.MDC; import java.io.IOException; /** * MDC 过滤器, 用于写入 TraceId, 请求 IP, 用户名等信息到日志中. * * @author zhaojun */ @WebFilter(urlPatterns = "/*") public class MDCFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; MDC.put(MdcConstant.TRACE_ID, IdUtil.fastUUID()); MDC.put(MdcConstant.IP, JakartaServletUtil.getClientIP(httpServletRequest)); MDC.put(MdcConstant.USER, ZFileAuthUtil.getCurrentUserId().toString()); try { filterChain.doFilter(httpServletRequest, httpServletResponse); } finally { MDC.clear(); } } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/filter/SecurityFilter.java ================================================ package im.zhaojun.zfile.core.filter; import cn.hutool.extra.servlet.JakartaServletUtil; import cn.hutool.extra.spring.SpringUtil; import im.zhaojun.zfile.core.constant.RuleTypeConstant; import im.zhaojun.zfile.core.util.StringUtils; import im.zhaojun.zfile.core.util.matcher.IRuleMatcher; import im.zhaojun.zfile.core.util.matcher.RuleMatcherFactory; import im.zhaojun.zfile.module.config.model.dto.SystemConfigDTO; import im.zhaojun.zfile.module.config.service.SystemConfigService; import jakarta.servlet.*; import jakarta.servlet.annotation.WebFilter; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import java.io.IOException; import java.util.List; /** * 检测访问的 IP 和 UA 是否符合系统安全设置中的规则 * * @author zhaojun */ @WebFilter(urlPatterns = "/*") public class SecurityFilter implements Filter { private static volatile SystemConfigService systemConfigService; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; // 双重检测锁, 防止多次初始化 if (systemConfigService == null) { synchronized (this) { if (systemConfigService == null) { systemConfigService = SpringUtil.getBean(SystemConfigService.class); } } } SystemConfigDTO systemConfig = systemConfigService.getSystemConfig(); String accessIpBlocklist = systemConfig.getAccessIpBlocklist(); String accessUaBlocklist = systemConfig.getAccessUaBlocklist(); // 判断当前访问 IP 是否在黑名单中 String currentAccessIp = JakartaServletUtil.getClientIP(httpServletRequest); if (StringUtils.isNotBlank(accessIpBlocklist) && checkIsDisableIP(accessIpBlocklist, currentAccessIp)) { httpServletResponse.setStatus(HttpStatus.FORBIDDEN.value()); httpServletResponse.getWriter().write("disable access.[" + currentAccessIp + "]"); return; } // 判断当前访问 User-Agent 是否在黑名单中 String userAgent = httpServletRequest.getHeader(HttpHeaders.USER_AGENT); if (StringUtils.isNotBlank(accessUaBlocklist) && checkIsDisableUA(accessUaBlocklist, userAgent)) { httpServletResponse.setStatus(HttpStatus.FORBIDDEN.value()); httpServletResponse.getWriter().write("disable access.[" + userAgent + "]"); return; } filterChain.doFilter(httpServletRequest, httpServletResponse); } private boolean checkIsDisableIP(String accessIpBlocklist, String currentAccessIp) { IRuleMatcher ruleMatcher = RuleMatcherFactory.getRuleMatcher(RuleTypeConstant.IP); List ruleList = StringUtils.split(accessIpBlocklist, StringUtils.LF); return ruleMatcher.matchAny(ruleList, currentAccessIp); } private boolean checkIsDisableUA(String accessUaBlocklist, String currentAccessUA) { IRuleMatcher ruleMatcher = RuleMatcherFactory.getRuleMatcher(RuleTypeConstant.SPRING_SIMPLE); List ruleList = StringUtils.split(accessUaBlocklist, StringUtils.LF); return ruleMatcher.matchAny(ruleList, currentAccessUA); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/io/EnsureContentLengthInputStreamResource.java ================================================ /* * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package im.zhaojun.zfile.core.io; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.InputStreamResource; import java.io.InputStream; /** * * 自定义 EnsureContentLengthInputStreamResource 可以保证必须实现 InputStream 的 contentLength 方法返回实际的长度. * 此类相较于 {@link org.springframework.core.io.InputStreamResource} 仅实现了 contentLength 方法. *

* {@link org.springframework.core.io.Resource} implementation for a given {@link InputStream}. *

Should only be used if no other specific {@code Resource} implementation * is applicable. In particular, prefer {@link ByteArrayResource} or any of the * file-based {@code Resource} implementations where possible. * *

In contrast to other {@code Resource} implementations, this is a descriptor * for an already opened resource - therefore returning {@code true} from * {@link #isOpen()}. Do not use an {@code InputStreamResource} if you need to * keep the resource descriptor somewhere, or if you need to read from a stream * multiple times. * * @author Juergen Hoeller * @author Sam Brannen * @since 28.12.2003 * @see ByteArrayResource * @see org.springframework.core.io.ClassPathResource * @see org.springframework.core.io.FileSystemResource * @see org.springframework.core.io.UrlResource */ public class EnsureContentLengthInputStreamResource extends InputStreamResource { private final long contentLength; /** * Create a new InputStreamResource. * @param inputStream the InputStream to use */ public EnsureContentLengthInputStreamResource(InputStream inputStream, long contentLength) { super(inputStream); this.contentLength = contentLength; } @Override public long contentLength() { return contentLength; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/model/request/PageQueryRequest.java ================================================ package im.zhaojun.zfile.core.model.request; import com.baomidou.mybatisplus.core.metadata.OrderItem; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.Objects; /** * 通用分页请求对象,可继承该类增加业务字段. * * @author zhaojun */ @Data public class PageQueryRequest { @Schema(title="分页页数") private Integer page = 1; @Schema(title="每页条数") private Integer limit = 10; @Schema(title="排序字段") private String orderBy = "create_date"; @Schema(title="排序顺序") private String orderDirection = "desc"; public OrderItem getOrderItem() { boolean asc = Objects.equals(orderDirection, "asc"); return asc ? OrderItem.asc(orderBy) : OrderItem.desc(orderBy); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/util/AjaxJson.java ================================================ package im.zhaojun.zfile.core.util; import im.zhaojun.zfile.core.exception.ErrorCode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.ToString; import java.io.Serializable; /** * ajax 请求返回 JSON 格式数据的封装 * * @author zhaojun */ @Data @ToString public class AjaxJson implements Serializable { private static final long serialVersionUID = 1L; // 序列化版本号 public static final String CODE_SUCCESS = "0"; // 成功状态码 @Schema(title = "业务状态码,0 为正常,其他值均为异常,异常情况下见响应消息", example = "0") private final String code; @Schema(title = "响应消息", example = "ok") private String msg; @Schema(title = "响应数据") private T data; @Schema(title = "数据总条数,分页情况有效") private final Long dataCount; @Schema(title = "跟踪 ID") private String traceId; public AjaxJson(String code, String msg) { if (code == null) { code = ErrorCode.SYSTEM_ERROR.getCode(); } this.code = code; this.msg = msg; this.dataCount = null; } public AjaxJson(String code, String msg, T data) { this.code = code; this.msg = msg; this.data = data; this.dataCount = null; } public AjaxJson(String code, String msg, T data, Long dataCount) { this.code = code; this.msg = msg; this.data = data; this.dataCount = dataCount; } // 返回成功 public static AjaxJson getSuccess() { return new AjaxJson<>(CODE_SUCCESS, "ok"); } public static AjaxJson getSuccess(String msg) { return new AjaxJson<>(CODE_SUCCESS, msg); } public static AjaxJson getSuccess(String msg, T data) { return new AjaxJson<>(CODE_SUCCESS, msg, data); } public static AjaxJson getSuccessData(T data) { return new AjaxJson<>(CODE_SUCCESS, "ok", data); } // 返回分页和数据的 public static AjaxJson getPageData(Long dataCount, T data) { return new AjaxJson<>(CODE_SUCCESS, "ok", data, dataCount); } // 返回错误 public static AjaxJson getError(String msg) { return new AjaxJson<>(ErrorCode.SYSTEM_ERROR.getCode(), msg); } // 返回未登录 public static AjaxJson getUnauthorizedResult() { return new AjaxJson<>(ErrorCode.BIZ_UNAUTHORIZED.getCode(), "未登录,请登录后再次访问"); } // 返回没权限的 public static AjaxJson getForbiddenResult() { return new AjaxJson<>(ErrorCode.NO_FORBIDDEN.getCode(), "未授权,请登录正确权限账号再试"); } // 返回未找到的 public static AjaxJson getNotFoundResult() { return new AjaxJson<>(ErrorCode.BIZ_NOT_FOUND.getCode(), ErrorCode.BIZ_NOT_FOUND.getMessage()); } public static AjaxJson getError(String code, String msg) { return new AjaxJson<>(code, msg); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/util/ArrayUtils.java ================================================ package im.zhaojun.zfile.core.util; /** * 数组工具类 * * @author zhaojun */ public class ArrayUtils { /** * 数组是否为空 * * @param * 数组元素类型 * * @param array * 数组 * * @return 是否为空 */ public static boolean isEmpty(T[] array) { return array == null || array.length == 0; } /** * 数组是否不为空 * * @param * 数组元素类型 * * @param array * 数组 * * @return 是否不为空 */ public static boolean isNotEmpty(T[] array) { return !isEmpty(array); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/util/CharPool.java ================================================ package im.zhaojun.zfile.core.util; public interface CharPool { /** * CHAR 常量:斜杠 {@code '/'} ASCII 47 */ char SLASH_CHAR = '/'; } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/util/CharSequenceUtil.java ================================================ package im.zhaojun.zfile.core.util; import cn.hutool.core.text.StrSplitter; import jakarta.annotation.Nullable; import org.apache.commons.lang3.StringUtils; import java.util.Collection; import java.util.List; /** * 字符串工具类 * * @author zhaojun */ public class CharSequenceUtil implements CharPool { /** * 找不到索引时的返回值 */ public static final int INDEX_NOT_FOUND = -1; /** * 字符串常量:{@code "null"}
* 注意:{@code "null" != null} */ public static final String NULL = "null"; /** * 字符串常量:空字符串 {@code ""} */ public static final String EMPTY = ""; /** * 字符串常量:空格符 {@code " "} */ public static final String SPACE = " "; /** * 获取 CharSequence 的长度, 如果为 null, 返回 0 * * @param ch * 要获取长度的 CharSequence, 可能为 null * * @return CharSequence 的长度 */ public static int length(final @Nullable CharSequence ch) { return ch == null ? 0 : ch.length(); } /** * {@link CharSequence} 转为字符串 * * @param cs * {@link CharSequence} * * @return 字符串 */ public static String str(final @Nullable CharSequence cs) { return null == cs ? null : cs.toString(); } /** * 判断 CharSequence 是否为空 * * @param cs * {@link CharSequence} * * @return 是否为空 */ public static boolean isEmpty(final @Nullable CharSequence cs) { return cs == null || cs.isEmpty(); } /** * CharSequence 是否不为空 * * @param cs * {@link CharSequence} * * @return 是否不为空 */ public static boolean isNotEmpty(final @Nullable CharSequence cs) { return !isEmpty(cs); } /** *

指定字符串数组中的元素,是否全部为空字符串。

*

如果指定的字符串数组的长度为 0,或者所有元素都是空字符串,则返回 true。

*
* *

例:

*
    *
  • {@code CharSequenceUtil.isAllEmpty() // true}
  • *
  • {@code CharSequenceUtil.isAllEmpty("", null) // true}
  • *
  • {@code CharSequenceUtil.isAllEmpty("123", "") // false}
  • *
  • {@code CharSequenceUtil.isAllEmpty("123", "abc") // false}
  • *
  • {@code CharSequenceUtil.isAllEmpty(" ", "\t", "\n") // false}
  • *
* * @param strs * 字符串列表 * * @return 所有字符串是否都为空 */ public static boolean isAllEmpty(final @Nullable CharSequence... strs) { if (strs == null) { return true; } for (CharSequence str : strs) { if (isNotEmpty(str)) { return false; } } return true; } /** *

是否包含空字符串。

*

如果指定的字符串数组的长度为 0,或者其中的任意一个元素是空字符串,则返回 true。

*
* *

例:

*
    *
  • {@code CharSequenceUtil.hasEmpty() // true}
  • *
  • {@code CharSequenceUtil.hasEmpty("", null) // true}
  • *
  • {@code CharSequenceUtil.hasEmpty("123", "") // true}
  • *
  • {@code CharSequenceUtil.hasEmpty("123", "abc") // false}
  • *
  • {@code CharSequenceUtil.hasEmpty(" ", "\t", "\n") // false}
  • *
* * @param strs * 字符串列表 * * @return 是否包含空字符串 */ public static boolean hasEmpty(final @Nullable CharSequence... strs) { if (ArrayUtils.isEmpty(strs)) { return true; } for (CharSequence str : strs) { if (isEmpty(str)) { return true; } } return false; } /** *

指定字符串数组中的元素,是否都不为空字符串。

*

如果指定的字符串数组的长度不为 0,或者所有元素都不是空字符串,则返回 true。

*
* *

例:

*
    *
  • {@code CharSequenceUtil.isAllNotEmpty() // false}
  • *
  • {@code CharSequenceUtil.isAllNotEmpty("", null) // false}
  • *
  • {@code CharSequenceUtil.isAllNotEmpty("123", "") // false}
  • *
  • {@code CharSequenceUtil.isAllNotEmpty("123", "abc") // true}
  • *
  • {@code CharSequenceUtil.isAllNotEmpty(" ", "\t", "\n") // true}
  • *
* * @param args * 字符串数组 * * @return 所有字符串是否都不为为空白 */ public static boolean isAllNotEmpty(final @Nullable CharSequence... args) { return !hasEmpty(args); } /** * 字符串是否为空白 * * @param ch * 要判断的字符串, 可能为 null * * @return 是否为空白 */ public static boolean isBlank(final @Nullable CharSequence ch) { final int strLen = ch == null ? 0 : ch.length(); if (strLen == 0) { return true; } for (int i = 0; i < strLen; i++) { if (!Character.isWhitespace(ch.charAt(i))) { return false; } } return true; } /** * 字符串是否不为空白 * * @param cs * 字符串 * * @return 是否不为空白 */ public static boolean isNotBlank(final @Nullable CharSequence cs) { return !isBlank(cs); } /** * 比较两个 CharSequence 是否相等, 区分大小写, 如果两个都为 null, 返回 true * * @param cs1 * CharSequence 1, 可能为 null * * @param cs2 * CharSequence 2, 可能为 null * * @return 是否相等 */ public static boolean equals(final @Nullable CharSequence cs1, final @Nullable CharSequence cs2) { if (cs1 == cs2) { return true; } if (cs1 == null || cs2 == null) { return false; } if (cs1.length() != cs2.length()) { return false; } if (cs1 instanceof String && cs2 instanceof String) { return cs1.equals(cs2); } // 逐个比较 final int length = cs1.length(); for (int i = 0; i < length; i++) { if (cs1.charAt(i) != cs2.charAt(i)) { return false; } } return true; } /** * 比较两个 CharSequence 是否相等, 可以选择是否忽略大小写, 如果两个都为 null, 返回 true * * @param cs1 * 字符串 1 * * @param cs2 * 字符串 2 * * @param ignoreCase * 是否忽略大小写 * * @return 是否相等 */ public static boolean equals(final @Nullable CharSequence cs1,final @Nullable CharSequence cs2, boolean ignoreCase) { return ignoreCase ? equalsIgnoreCase(cs1, cs2) : equals(cs1, cs2); } /** * 字符串是否相等, 忽略大小写 * * @param cs1 * 字符串 1 * * @param cs2 * 字符串 2 * * @return 忽略大小写后是否相等 */ public static boolean equalsIgnoreCase(final @Nullable CharSequence cs1, final @Nullable CharSequence cs2) { if (cs1 == cs2) { return true; } if (cs1 == null || cs2 == null) { return false; } if (cs1.length() != cs2.length()) { return false; } return cs1.toString().equalsIgnoreCase(cs2.toString()); } /** * 切分字符串,如果分隔符不存在则返回原字符串 * * @param str * 被切分的字符串 * * @param separator * 分隔符 * * @return 字符串 */ public static List split(final CharSequence str, final CharSequence separator) { return split(str, separator, false, false); } /** * 切分字符串 * * @param str * 被切分的字符串 * * @param separator * 分隔符字符 * * @param isTrim * 是否去除切分字符串后每个元素两边的空格 * * @param ignoreEmpty * 是否忽略空串 * * @return 切分后的集合 */ public static List split(CharSequence str, CharSequence separator, boolean isTrim, boolean ignoreEmpty) { return split(str, separator, 0, isTrim, ignoreEmpty); } /** * 切分字符串 * * @param str * 被切分的字符串 * * @param separator * 分隔符字符 * * @param limit * 限制分片数,-1 不限制 * * @param isTrim * 是否去除切分字符串后每个元素两边的空格 * * @param ignoreEmpty * 是否忽略空串 * * @return 切分后的集合 */ public static List split(CharSequence str, CharSequence separator, int limit, boolean isTrim, boolean ignoreEmpty) { final String separatorStr = (null == separator) ? null : separator.toString(); return StrSplitter.split(str, separatorStr, limit, isTrim, ignoreEmpty); } /** * 指定字符串是否在字符串中出现过 * * @param str * 字符串 * * @param searchStr * 被查找的字符串 * * @return 是否包含 */ public static boolean contains(final @Nullable CharSequence str, final @Nullable CharSequence searchStr) { if (null == str || null == searchStr) { return false; } return str.toString().contains(searchStr); } /** * 查找指定字符串是否包含指定字符串列表中的任意一个字符串 * * @param str * 指定字符串 * * @param testStrs * 需要检查的字符串数组 * * @return 是否包含任意一个字符串 */ public static boolean containsAny(final @Nullable CharSequence str, final @Nullable CharSequence... testStrs) { if (isEmpty(str) || ArrayUtils.isEmpty(testStrs)) { return false; } for (CharSequence checkStr : testStrs) { if (null != checkStr && str.toString().contains(checkStr)) { return true; } } return false; } /** * 查找指定字符串是否包含指定字符串列表中的任意一个字符串
* 忽略大小写 * * @param str * 指定字符串 * * @param testStrs * 需要检查的字符串数组 * * @return 是否包含任意一个字符串 */ public static boolean containsAnyIgnoreCase(final @Nullable CharSequence str, final @Nullable CharSequence... testStrs) { return StringUtils.containsAnyIgnoreCase(str, testStrs); } /** * 以 conjunction 为分隔符将多个对象转换为字符串 * * @param conjunction * 分隔符 * * @param objs * 数组 * * @return 连接后的字符串 */ public static String join(CharSequence conjunction, Object... objs) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < objs.length; i++) { Object item = objs[i]; sb.append(item); if (i < objs.length - 1) { sb.append(conjunction); } } return sb.toString(); } /** * 以 conjunction 为分隔符将 Collection 对象转换为字符串 * * @param conjunction * 分隔符 * * @param collection * 集合 * * @return 连接后的字符串 */ public static String join(CharSequence conjunction, Collection collection) { StringBuilder sb = new StringBuilder(); for (Object item : collection) { sb.append(item).append(conjunction); } if (!sb.isEmpty()) { sb.delete(sb.length() - conjunction.length(), sb.length()); } return sb.toString(); } /** * 是否以指定字符串开头 * * @param str * 被监测字符串 * * @param prefix * 开头字符串 * * @return 是否以指定字符串开头 */ public static boolean startWith(CharSequence str, CharSequence prefix) { return startWith(str, prefix, false); } /** * 是否以指定字符串开头,忽略大小写 * * @param str * 被监测字符串 * * @param prefix * 开头字符串 * * @return 是否以指定字符串开头 */ public static boolean startWithIgnoreCase(CharSequence str, CharSequence prefix) { return startWith(str, prefix, true); } /** * 是否以指定字符串开头
* 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false * * @param str * 被监测字符串 * * @param prefix * 开头字符串 * * @param ignoreCase * 是否忽略大小写 * * @return 是否以指定字符串开头 */ public static boolean startWith(CharSequence str, CharSequence prefix, boolean ignoreCase) { return startWith(str, prefix, ignoreCase, false); } /** * 是否以指定字符串开头
* 如果给定的字符串和开头字符串都为 null 则返回 true,否则任意一个值为 null 返回 false
*
     *     CharSequenceUtil.startWith("123", "123", false, true);   -- false
     *     CharSequenceUtil.startWith("ABCDEF", "abc", true, true); -- true
     *     CharSequenceUtil.startWith("abc", "abc", true, true);    -- false
     * 
* * @param str * 被监测字符串 * * @param prefix * 开头字符串 * * @param ignoreCase * 是否忽略大小写 * * @param ignoreEquals * 是否忽略字符串相等的情况 * * @return 是否以指定字符串开头 */ public static boolean startWith(final @Nullable CharSequence str, final @Nullable CharSequence prefix, boolean ignoreCase, boolean ignoreEquals) { if (null == str || null == prefix) { if (ignoreEquals) { return false; } return null == str && null == prefix; } boolean isStartWith = str.toString() .regionMatches(ignoreCase, 0, prefix.toString(), 0, prefix.length()); if (isStartWith) { return (!ignoreEquals) || (!equals(str, prefix, ignoreCase)); } return false; } /** * 是否以指定字符串结尾 * * @param str * 被监测字符串 * * @param suffix * 结尾字符串 * * @return 是否以指定字符串结尾 */ public static boolean endWith(final @Nullable CharSequence str, final @Nullable CharSequence suffix) { return endWith(str, suffix, false); } /** * 是否以指定字符串结尾
* 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false * * @param str * 被监测字符串 * * @param suffix * 结尾字符串 * * @param ignoreCase * 是否忽略大小写 * * @return 是否以指定字符串结尾 */ public static boolean endWith(final @Nullable CharSequence str, final @Nullable CharSequence suffix, boolean ignoreCase) { return endWith(str, suffix, ignoreCase, false); } /** * 是否以指定字符串结尾
* 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false * * @param str * 被监测字符串 * * @param suffix * 结尾字符串 * * @param ignoreCase * 是否忽略大小写 * * @param ignoreEquals * 是否忽略字符串相等的情况 * * @return 是否以指定字符串结尾 */ public static boolean endWith(final @Nullable CharSequence str, final @Nullable CharSequence suffix, boolean ignoreCase, boolean ignoreEquals) { if (null == str || null == suffix) { if (ignoreEquals) { return false; } return null == str && null == suffix; } final int strOffset = str.length() - suffix.length(); boolean isEndWith = str.toString() .regionMatches(ignoreCase, strOffset, suffix.toString(), 0, suffix.length()); if (isEndWith) { return (!ignoreEquals) || (!equals(str, suffix, ignoreCase)); } return false; } /** * 去掉指定前缀 * * @param str * 字符串 * * @param prefix * 前缀 * * @return 切掉后的字符串,若前缀不是 preffix, 返回原字符串 */ public static String removePrefix(final @Nullable CharSequence str, final @Nullable CharSequence prefix) { if (isEmpty(str) || isEmpty(prefix)) { return str(str); } String str2 = str.toString(); String prefix2 = prefix.toString(); if (str2.startsWith(prefix2)) { return str.subSequence(prefix.length(), str.length()).toString(); } return str2; // 若前缀不是 prefix,返回原字符串 } /** * 返回第一个非 {@code null} 元素 * * @param strs * 多个元素 * * @param * 元素类型 * * @return 第一个非空元素,如果给定的数组为空或者都为空,返回{@code null} */ @SuppressWarnings("unchecked") public static T firstNonNull(T... strs) { if (ArrayUtils.isNotEmpty(strs)) { for (T str : strs) { if (isNotEmpty(str)) { return str; } } } return null; } /** * 截取分隔字符串之前的字符串,不包括分隔字符串
* 如果给定的字符串为空串(null或"")或者分隔字符串为null,返回原字符串
* 如果分隔字符串为空串"",则返回空串,如果分隔字符串未找到,返回原字符串,举例如下: * *
     * CharSequenceUtil.subBefore(null, *, false)      = null
     * CharSequenceUtil.subBefore("", *, false)        = ""
     * CharSequenceUtil.subBefore("abc", "a", false)   = ""
     * CharSequenceUtil.subBefore("abcba", "b", false) = "a"
     * CharSequenceUtil.subBefore("abc", "c", false)   = "ab"
     * CharSequenceUtil.subBefore("abc", "d", false)   = "abc"
     * CharSequenceUtil.subBefore("abc", "", false)    = ""
     * CharSequenceUtil.subBefore("abc", null, false)  = "abc"
     * 
* * @param string * 被查找的字符串 * * @param separator * 分隔字符串(不包括) * * @param isLastSeparator * 是否查找最后一个分隔字符串(多次出现分隔字符串时选取最后一个),true为选取最后一个 * * @return 切割后的字符串 */ public static String subBefore(final @Nullable CharSequence string, final @Nullable CharSequence separator, boolean isLastSeparator) { if (isEmpty(string) || separator == null) { return null == string ? null : string.toString(); } final String str = string.toString(); final String sep = separator.toString(); if (sep.isEmpty()) { return EMPTY; } final int pos = isLastSeparator ? str.lastIndexOf(sep) : str.indexOf(sep); if (INDEX_NOT_FOUND == pos) { return str; } if (0 == pos) { return EMPTY; } return str.substring(0, pos); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/util/ClassUtils.java ================================================ package im.zhaojun.zfile.core.util; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; /** * Class & 反射相关工具类 * * @author zhaojun */ public class ClassUtils { public static Class forName(String className) { try { return Class.forName(className); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } /** * 获取指定类的泛型类型, 只获取第一个泛型类型 * * @param clazz * 泛型类 * * @return 泛型类型 */ public static Class getClassFirstGenericsParam(Class clazz) { Type genericSuperclass = clazz.getGenericSuperclass(); Type actualTypeArgument = ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0]; return (Class) actualTypeArgument; } public static Class getGenericType(Field field) { ParameterizedType listType = (ParameterizedType) field.getGenericType(); return (Class) listType.getActualTypeArguments()[0]; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/util/CollectionUtils.java ================================================ package im.zhaojun.zfile.core.util; import cn.hutool.core.lang.func.Func1; import javax.annotation.Nullable; import java.util.*; public class CollectionUtils { /** * 判断集合是否为空 * * @param collection * 集合 * * @return 是否为空 */ public static boolean isEmpty(@Nullable Collection collection) { return (collection == null || collection.isEmpty()); } /** * 判断集合是否不为空 * * @param collection * 集合 * * @return 是否不为空 */ public static boolean isNotEmpty(@Nullable Collection collection) { return !isEmpty(collection); } /** * 从集合中获取第一个元素, 如果集合为空则返回 {@code null} * * @param list * 集合,可能为 {@code null} * * @return 第一个元素,如果集合为空则返回 {@code null} */ @Nullable public static T getFirst(@Nullable List list) { if (isEmpty(list)) { return null; } return list.get(0); } /** * 从集合中获取最后一个元素, 如果集合为空则返回 {@code null} * * @param list * 集合,可能为 {@code null} * * @return 最后一个元素,如果集合为空则返回 {@code null} */ @Nullable public static T getLast(@Nullable List list) { if (isEmpty(list)) { return null; } return list.get(list.size() - 1); } /** * 加入全部 * * @param * 集合元素类型 * * @param collection * 被加入的集合 {@link Collection} * * @param values * 要加入的内容数组 * * @return 原集合 */ public static Collection addAll(Collection collection, T[] values) { if (null != collection && null != values) { Collections.addAll(collection, values); } return collection; } /** * Iterable 转换为 Map, 根据指定的 keyFunc 函数生成 Key. Value 为 Iterable 中的元素.
* 可以指定将结果放入的 Map, 如不指定则会新建一个 HashMap 返回. * * @param * Map Key 类型 * * @param * Map Value 类型 * * @param values * 被转换的 Iterable * * @param map * 转换后的 Value 存放的 Map, 如果为 {@code null} 则新建一个 HashMap * * @param keyFunc * 生成 Map 的 Key 的函数 * * @return 转换后的 Map */ public static Map toMap(final @Nullable Iterable values, final @Nullable Map map, final @Nullable Func1 keyFunc) { if (values == null || keyFunc == null) { return Collections.emptyMap(); } final Map result = map == null ? new HashMap<>() : map; for (V value : values) { try { result.put(keyFunc.call(value), value); } catch (Exception e) { throw new RuntimeException(e); } } return result; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/util/DnsUtil.java ================================================ package im.zhaojun.zfile.core.util; import com.alibaba.dcm.DnsCacheManipulator; import com.alibaba.fastjson2.JSONArray; import org.springframework.lang.Nullable; public class DnsUtil { /** * 通过 HTTP DNS 获取域名对应的 IP 地址 * * @param domain * 域名 * * @return IP 地址数组 */ public static @Nullable String[] getDomainIpByHttpDns(String domain) { String jsonArrayStr = cn.hutool.http.HttpUtil.get("http://223.5.5.5/resolve?name=" + domain + "&short=1", 3000); JSONArray jsonArray = JSONArray.parseArray(jsonArrayStr); if (!jsonArray.isEmpty()) { String[] result = new String[jsonArray.size()]; for (int i = 0; i < jsonArray.size(); i++) { result[i] = jsonArray.getString(i); } return result; } else { return null; } } /** * 通过 HTTP DNS 获取域名对应的 IP 地址, 并设置 DNS 缓存. * * @param domain * 域名 * * @param cacheTime * 缓存时间, 单位: 毫秒 * * @return IP 地址数组 */ public static String[] getDomainIpByHttpDnsAndCache(String domain, int cacheTime) { String[] domainIpByHttpDns = getDomainIpByHttpDns(domain); if (domainIpByHttpDns != null) { // 设置 DNS 缓存 DnsCacheManipulator.setDnsCache(cacheTime, domain, domainIpByHttpDns); } return domainIpByHttpDns; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/util/EnumConvertUtils.java ================================================ package im.zhaojun.zfile.core.util; import cn.hutool.core.convert.Convert; import cn.hutool.core.util.ClassUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ReflectUtil; import com.baomidou.mybatisplus.annotation.EnumValue; import com.fasterxml.jackson.annotation.JsonValue; import java.lang.reflect.Field; /** * 枚举转换工具类 * * @author zhaojun */ public class EnumConvertUtils { /** * 根据枚举 class 和值获取对应的枚举对象 * * @param clazz * 枚举类 Class * * @param value * 枚举值 * * @return 枚举对象 */ public static Enum convertStrToEnum(Class clazz, Object value) { if (!ClassUtil.isEnum(clazz)) { return null; } Field[] fields = ReflectUtil.getFields(clazz); for (Field field : fields) { boolean jsonValuePresent = field.isAnnotationPresent(JsonValue.class); boolean enumValuePresent = field.isAnnotationPresent(EnumValue.class); if (jsonValuePresent || enumValuePresent) { Object[] enumConstants = clazz.getEnumConstants(); for (Object enumObj : enumConstants) { if (ObjectUtil.equal(value, ReflectUtil.getFieldValue(enumObj, field))) { return (Enum) enumObj; } } } } return null; } /** * 转换枚举对象为字符串, 如果枚举对象没有定义 JsonValue 注解, 则使用 EnumValue 注解的值 * * @param enumObj * 枚举对象 * * @return 字符串 */ public static String convertEnumToStr(Object enumObj) { Class clazz = enumObj.getClass(); if (!ClassUtil.isEnum(clazz)) { return null; } Field[] fields = ReflectUtil.getFields(clazz); for (Field field : fields) { boolean jsonValuePresent = field.isAnnotationPresent(JsonValue.class); boolean enumValuePresent = field.isAnnotationPresent(EnumValue.class); if (jsonValuePresent || enumValuePresent) { return Convert.toStr(ReflectUtil.getFieldValue(enumObj, field)); } } return null; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/util/FileComparator.java ================================================ package im.zhaojun.zfile.core.util; import cn.hutool.core.comparator.CompareUtil; import im.zhaojun.zfile.module.storage.model.result.FileItemResult; import im.zhaojun.zfile.module.storage.model.enums.FileTypeEnum; import java.util.Comparator; /** * 文件比较器 *
    *
  • 文件夹始终比文件排序高
  • *
  • 默认按照名称排序
  • *
  • 默认排序为升序
  • *
  • 按名称排序不区分大小写
  • *
* @author zhaojun */ public class FileComparator implements Comparator { private String sortBy; private String order; public FileComparator(String sortBy, String order) { this.sortBy = sortBy; this.order = order; } /** * 比较两个文件的大小 * * @param o1 * 第一个文件 * * @param o2 * 第二个文件 * * @return 比较结果 */ @Override public int compare(FileItemResult o1, FileItemResult o2) { if (sortBy == null) { sortBy = "name"; } if (order == null) { order = "asc"; } FileTypeEnum o1Type = o1.getType(); FileTypeEnum o2Type = o2.getType(); NaturalOrderComparator naturalOrderComparator = new NaturalOrderComparator(); if (o1Type.equals(o2Type)) { int result = switch (sortBy) { case "time" -> CompareUtil.compare(o1.getTime(), o2.getTime()); case "size" -> CompareUtil.compare(o1.getSize(), o2.getSize()); default -> naturalOrderComparator.compare(o1.getName(), o2.getName()); }; return "asc".equals(order) ? result : -result; } if (o1Type.equals(FileTypeEnum.FOLDER)) { return -1; } else { return 1; } } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/util/FileResponseUtil.java ================================================ package im.zhaojun.zfile.core.util; import cn.hutool.core.io.FileUtil; import im.zhaojun.zfile.core.exception.ErrorCode; import im.zhaojun.zfile.core.exception.status.NotFoundAccessException; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import java.io.File; import java.nio.charset.StandardCharsets; /** * 将文件输出对象 * * @author zhaojun */ @Slf4j public class FileResponseUtil { /** * 文件下载,单线程,不支持断点续传 * * @param file * 文件对象 * * @param fileName * 要保存为的文件名 * * @return 文件下载对象 */ public static ResponseEntity exportSingleThread(File file, String fileName) { if (!file.exists()) { throw new NotFoundAccessException(ErrorCode.BIZ_FILE_NOT_EXIST); } MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM; HttpHeaders headers = new HttpHeaders(); if (StringUtils.isEmpty(fileName)) { fileName = file.getName(); } ContentDisposition contentDisposition = ContentDisposition .builder("inline") .filename(fileName, StandardCharsets.UTF_8) .build(); headers.setContentDisposition(contentDisposition); return ResponseEntity .ok() .headers(headers) .contentLength(file.length()) .contentType(mediaType) .body(new InputStreamResource(FileUtil.getInputStream(file))); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/util/FileSizeConverter.java ================================================ package im.zhaojun.zfile.core.util; import java.util.regex.Matcher; import java.util.regex.Pattern; public class FileSizeConverter { private static final long KB_FACTOR = 1024L; private static final long MB_FACTOR = 1024L * KB_FACTOR; private static final long GB_FACTOR = 1024L * MB_FACTOR; private static final long TB_FACTOR = 1024L * GB_FACTOR; private static final long PB_FACTOR = 1024L * TB_FACTOR; private static final Pattern FILE_SIZE_PATTERN = Pattern.compile("([\\d.]+)\\s*([a-zA-Z]+)"); public static long convertFileSizeToBytes(String sizeStr) { if (sizeStr == null || sizeStr.trim().isEmpty()) { throw new IllegalArgumentException("输入字符串不能为空"); } Matcher matcher = FILE_SIZE_PATTERN.matcher(sizeStr.trim()); if (!matcher.matches()) { throw new IllegalArgumentException("无效的文件大小格式: " + sizeStr); } String valueStr = matcher.group(1); String unitStr = matcher.group(2).toUpperCase(); double value; try { value = Double.parseDouble(valueStr); } catch (NumberFormatException e) { throw new IllegalArgumentException("无效的数字格式: " + valueStr, e); } if (value < 0) { throw new IllegalArgumentException("文件大小不能为负数: " + valueStr); } long multiplier = switch (unitStr) { case "B" -> 1L; case "KB", "KIB" -> KB_FACTOR; case "MB", "MIB" -> MB_FACTOR; case "GB", "GIB" -> GB_FACTOR; case "TB", "TIB" -> TB_FACTOR; case "PB", "PIB" -> PB_FACTOR; default -> throw new IllegalArgumentException("不支持的单位: " + unitStr + " (支持 B, KB, MB, GB, TB, PB)"); }; double bytesDouble = value * multiplier; if (bytesDouble > Long.MAX_VALUE) { throw new ArithmeticException("转换后的字节数超过了 Long 类型的最大值: " + bytesDouble); } return Math.round(bytesDouble); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/util/FileUtils.java ================================================ package im.zhaojun.zfile.core.util; import org.apache.commons.io.FilenameUtils; /** * 文件相关工具类 * * @author zhaojun */ public class FileUtils { public static String getName(final String fileName) { if (fileName == null) { return null; } int i = fileName.lastIndexOf(CharSequenceUtil.SLASH_CHAR); if (i >= 0 && i <= fileName.length() - 1) { return fileName.substring(i + 1); } return fileName; } public static String getParentPath(final String fileName) { String fullPathNoEndSeparator = FilenameUtils.getFullPathNoEndSeparator(StringUtils.trimEndSlashes(fileName)); if (fullPathNoEndSeparator == null || fullPathNoEndSeparator.isEmpty()) { return StringUtils.SLASH; } return fullPathNoEndSeparator; } public static String getExtension(final String fileName) throws IllegalArgumentException { if (fileName == null) { return null; } int i = fileName.lastIndexOf('.'); if (i > 0 && i < fileName.length() - 1) { return fileName.substring(i + 1); } return ""; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/util/HttpUtil.java ================================================ package im.zhaojun.zfile.core.util; import im.zhaojun.zfile.core.constant.ZFileConstant; import im.zhaojun.zfile.core.exception.ErrorCode; import im.zhaojun.zfile.core.exception.biz.GetPreviewTextContentBizException; import im.zhaojun.zfile.core.exception.core.BizException; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.net.URL; import java.net.URLConnection; /** * 网络相关工具 * * @author zhaojun */ @Slf4j public class HttpUtil { /** * 获取 URL 对应的文件内容 * * @param url * 文件 URL * * @return 文件内容 */ public static String getTextContent(String url) { long maxFileSize = 1024 * ZFileConstant.TEXT_MAX_FILE_SIZE_KB; if (getRemoteFileSize(url) > maxFileSize) { throw new BizException(ErrorCode.BIZ_PREVIEW_FILE_SIZE_EXCEED); } String result; try { result = cn.hutool.http.HttpUtil.get(url); } catch (Exception e) { throw new GetPreviewTextContentBizException(url, e); } return result == null ? "" : result; } /** * 获取远程文件大小 * * @param url * 文件 URL * * @return 文件大小 */ public static Long getRemoteFileSize(String url) { long size = 0; URL urlObject; try { urlObject = new URL(url); URLConnection conn = urlObject.openConnection(); size = conn.getContentLength(); } catch (IOException e) { e.printStackTrace(); } return size; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/util/NaturalOrderComparator.java ================================================ package im.zhaojun.zfile.core.util; /* NaturalOrderComparator.java -- Perform 'natural order' comparisons of strings in Java. Copyright (C) 2003 by Pierre-Luc Paour Based on the C version by Martin Pool, of which this is more or less a straight conversion. Copyright (C) 2000 by Martin Pool This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. */ import java.util.Comparator; /** * 类 windows 文件排序算法 * * @author zhaojun */ public class NaturalOrderComparator implements Comparator { private static final char ZERO_CHAR = '0'; private int compareRight(String a, String b) { int bias = 0, ia = 0, ib = 0; // The longest run of digits wins. That aside, the greatest // value wins, but we can't know that it will until we've scanned // both numbers to know that they have the same magnitude, so we // remember it in BIAS. for (; ; ia++, ib++) { char ca = charAt(a, ia); char cb = charAt(b, ib); if (!isDigit(ca) && !isDigit(cb)) { return bias; } if (!isDigit(ca)) { return -1; } if (!isDigit(cb)) { return +1; } if (ca == 0 && cb == 0) { return bias; } if (bias == 0) { if (ca < cb) { bias = -1; } else if (ca > cb) { bias = +1; } } } } @Override public int compare(String a, String b) { int ia = 0, ib = 0; int nza, nzb; char ca, cb; while (true) { // Only count the number of zeroes leading the last number compared nza = nzb = 0; ca = charAt(a, ia); cb = charAt(b, ib); // skip over leading spaces or zeros while (Character.isSpaceChar(ca) || ca == ZERO_CHAR) { if (ca == ZERO_CHAR) { nza++; } else { // Only count consecutive zeroes nza = 0; } ca = charAt(a, ++ia); } while (Character.isSpaceChar(cb) || cb == '0') { if (cb == '0') { nzb++; } else { // Only count consecutive zeroes nzb = 0; } cb = charAt(b, ++ib); } // Process run of digits if (Character.isDigit(ca) && Character.isDigit(cb)) { int bias = compareRight(a.substring(ia), b.substring(ib)); if (bias != 0) { return bias; } } if (ca == 0 && cb == 0) { // The strings compare the same. Perhaps the caller // will want to call strcmp to break the tie. return compareEqual(a, b, nza, nzb); } if (ca < cb) { return -1; } if (ca > cb) { return +1; } ++ia; ++ib; } } private static boolean isDigit(char c) { return Character.isDigit(c) || c == '.' || c == ','; } private static char charAt(String s, int i) { return i >= s.length() ? 0 : s.charAt(i); } private static int compareEqual(String a, String b, int nza, int nzb) { if (nza - nzb != 0) { return nza - nzb; } if (a.length() == b.length()) { return a.compareTo(b); } return a.length() - b.length(); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/util/NumberUtils.java ================================================ package im.zhaojun.zfile.core.util; /** * 数字工具类 * * @author zhaojun */ public class NumberUtils { public static boolean isNullOrZero(Integer number) { return number == null || number == 0; } public static boolean isNotNullOrZero(Integer number) { return number != null && number != 0; } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/util/OnlyOfficeKeyCacheUtils.java ================================================ package im.zhaojun.zfile.core.util; import cn.hutool.cache.Cache; import cn.hutool.cache.CacheUtil; import cn.hutool.cache.impl.CacheObj; import im.zhaojun.zfile.module.onlyoffice.model.OnlyOfficeFile; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.RandomStringUtils; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; /** * OnlyOffice 文件信息与 Key 缓存工具类 * * @author zhaojun */ @Slf4j public class OnlyOfficeKeyCacheUtils { /** * 存储 OnlyOffice 文件信息与 Key 的映射关系. 最多存储 10000 个 Key, 防止内存溢出. */ private static final Cache ONLY_OFFICE_FILE_KEY_MAP = CacheUtil.newLRUCache(10000); /** * 存储 OnlyOffice Key 与文件信息的映射关系. 最多存储 10000 个 Key, 防止内存溢出. */ private static final Cache ONLY_OFFICE_KEY_FILE_MAP = CacheUtil.newLRUCache(10000); /** * 存储文件锁, 防止并发操作文件缓存时出现问题. */ private static final Cache locks = CacheUtil.newLRUCache(300); /** * 获取该文件缓存的 key, 如果不存在则生成一个新的 key 并缓存. * * @param onlyOfficeFile * OnlyOffice 文件信息 * * @return 该文件唯一标识 */ public static String getKeyOrPutNew(OnlyOfficeFile onlyOfficeFile, long timeout) { ReentrantLock lock = getLock(onlyOfficeFile); try { boolean getLock = lock.tryLock(timeout, TimeUnit.MILLISECONDS); if (BooleanUtils.isFalse(getLock)) { log.warn("{} 尝试获取锁超时, 强制忽略锁直接操作文件.", onlyOfficeFile); } try { if (ONLY_OFFICE_FILE_KEY_MAP.containsKey(onlyOfficeFile)) { return ONLY_OFFICE_FILE_KEY_MAP.get(onlyOfficeFile); } else { String key = RandomStringUtils.randomAlphabetic(10); ONLY_OFFICE_FILE_KEY_MAP.put(onlyOfficeFile, key); ONLY_OFFICE_KEY_FILE_MAP.put(key, onlyOfficeFile); return key; } } finally { lock.unlock(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IllegalStateException("Thread was interrupted", e); } } /** * 清理缓存中的 Key 与文件信息的映射关系.(文件发生了变化, 需要重新生成 OnlyOffice 预览链接时调用) * * @param key * 文件唯一标识 */ public static OnlyOfficeFile removeByKey(String key) { OnlyOfficeFile onlyOfficeFile = ONLY_OFFICE_KEY_FILE_MAP.get(key); if (onlyOfficeFile == null) { return null; } ONLY_OFFICE_FILE_KEY_MAP.remove(onlyOfficeFile); ONLY_OFFICE_KEY_FILE_MAP.remove(key); return onlyOfficeFile; } /** * 清理缓存中的文件信息与 Key 的映射关系.(文件发生了变化, 需要重新生成 OnlyOffice 预览链接时调用) * * @param onlyOfficeFile * OnlyOffice 文件信息 */ public static OnlyOfficeFile removeByFile(OnlyOfficeFile onlyOfficeFile) { String key = ONLY_OFFICE_FILE_KEY_MAP.get(onlyOfficeFile); if (key == null) { return null; } ONLY_OFFICE_FILE_KEY_MAP.remove(onlyOfficeFile); ONLY_OFFICE_KEY_FILE_MAP.remove(key); return onlyOfficeFile; } /** * 清理缓存中的某个文件夹下所有文件信息与 Key 的映射关系.(文件发生了变化, 需要重新生成 OnlyOffice 预览链接时调用) * * @param onlyOfficeFile * OnlyOffice 文件信息 */ public static List removeByFolder(OnlyOfficeFile onlyOfficeFile) { List caches = new ArrayList<>(); Iterator> cacheObjIterator = ONLY_OFFICE_FILE_KEY_MAP.cacheObjIterator(); while (cacheObjIterator.hasNext()) { CacheObj cacheObj = cacheObjIterator.next(); OnlyOfficeFile cacheOnlyOfficeFile = cacheObj.getKey(); if (cacheOnlyOfficeFile.getStorageKey().equals(onlyOfficeFile.getStorageKey()) && StringUtils.startWith(cacheOnlyOfficeFile.getPathAndName(), onlyOfficeFile.getPathAndName())) { ONLY_OFFICE_FILE_KEY_MAP.remove(cacheObj.getKey()); ONLY_OFFICE_KEY_FILE_MAP.remove(cacheObj.getValue()); caches.add(cacheOnlyOfficeFile); } } return caches; } /** * 获取文件锁, 防止并发操作文件缓存时出现问题. * * @param key * 文件唯一标识 * * @return 锁对象 */ public static ReentrantLock getLock(OnlyOfficeFile key) { return locks.get(key, true, ReentrantLock::new); } } ================================================ FILE: src/main/java/im/zhaojun/zfile/core/util/PatternMatcherUtils.java ================================================ package im.zhaojun.zfile.core.util; import java.nio.file.FileSystems; import java.nio.file.PathMatcher; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; /** * 规则表达式工具类 * * @author zhaojun */ public class PatternMatcherUtils { private static final Map PATH_MATCHER_MAP = new HashMap<>(); /** * 兼容模式的 glob 表达式匹配. * 默认的 glob 表达式是不支持以下情况的:
*