Feature: add support for dogeCloud manage

ISSUES CLOSED: #73
This commit is contained in:
萌萌哒赫萝 2023-08-21 08:53:01 -07:00
parent 10da6a92d1
commit 3040f4bfbb
8 changed files with 87 additions and 17 deletions

View File

@ -576,6 +576,8 @@ MANAGE_CONSTANT_S3_BUCKET_DESC: Bucket name - Optional
MANAGE_CONSTANT_S3_BUCKET_PLACEHOLDER: English comma-separated list, e.g. bucket1,bucket2 MANAGE_CONSTANT_S3_BUCKET_PLACEHOLDER: English comma-separated list, e.g. bucket1,bucket2
MANAGE_CONSTANT_S3_BASE_DIR_DESC: Base directory - Optional MANAGE_CONSTANT_S3_BASE_DIR_DESC: Base directory - Optional
MANAGE_CONSTANT_S3_BASE_DIR_PLACEHOLDER: English comma-separated list, e.g. /dir1,/dir2 MANAGE_CONSTANT_S3_BASE_DIR_PLACEHOLDER: English comma-separated list, e.g. /dir1,/dir2
MANAGE_CONSTANT_S3_DOGE_CLOUD_SUPPORT_DESC: Enable Doge Cloud API
MANAGE_CONSTANT_S3_DOGE_CLOUD_SUPPORT_TOOLTIP: Support Doge Cloud API
MANAGE_CONSTANT_S3_PAGING_DESC: Enable pagination MANAGE_CONSTANT_S3_PAGING_DESC: Enable pagination
MANAGE_CONSTANT_S3_ITEMS_PAGE_DESC: Items per page MANAGE_CONSTANT_S3_ITEMS_PAGE_DESC: Items per page
MANAGE_CONSTANT_S3_EXPLAIN: When configuring bucket name and base directory, they can be set using English comma separation. The order must be consistent and missing or empty items will use the default value. MANAGE_CONSTANT_S3_EXPLAIN: When configuring bucket name and base directory, they can be set using English comma separation. The order must be consistent and missing or empty items will use the default value.

View File

@ -579,6 +579,8 @@ MANAGE_CONSTANT_S3_BUCKET_DESC: 存储桶名-可选
MANAGE_CONSTANT_S3_BUCKET_PLACEHOLDER: 英文逗号分隔例如bucket1,bucket2 MANAGE_CONSTANT_S3_BUCKET_PLACEHOLDER: 英文逗号分隔例如bucket1,bucket2
MANAGE_CONSTANT_S3_BASE_DIR_DESC: 起始目录-可选 MANAGE_CONSTANT_S3_BASE_DIR_DESC: 起始目录-可选
MANAGE_CONSTANT_S3_BASE_DIR_PLACEHOLDER: '英文逗号分隔,例如:/dir1,/dir2' MANAGE_CONSTANT_S3_BASE_DIR_PLACEHOLDER: '英文逗号分隔,例如:/dir1,/dir2'
MANAGE_CONSTANT_S3_DOGE_CLOUD_SUPPORT_DESC: 是否使用Doge Cloud
MANAGE_CONSTANT_S3_DOGE_CLOUD_SUPPORT_TOOLTIP: 开启后将使用Doge Cloud的API
MANAGE_CONSTANT_S3_PAGING_DESC: 是否开启分页 MANAGE_CONSTANT_S3_PAGING_DESC: 是否开启分页
MANAGE_CONSTANT_S3_ITEMS_PAGE_DESC: 每页显示数量 MANAGE_CONSTANT_S3_ITEMS_PAGE_DESC: 每页显示数量
MANAGE_CONSTANT_S3_EXPLAIN: 存储桶名和起始目录配置时可通过英文逗号分隔不同存储桶的设置,顺序必须一致,逗号间留空或缺失项使用默认值 MANAGE_CONSTANT_S3_EXPLAIN: 存储桶名和起始目录配置时可通过英文逗号分隔不同存储桶的设置,顺序必须一致,逗号间留空或缺失项使用默认值

View File

@ -576,6 +576,8 @@ MANAGE_CONSTANT_S3_BUCKET_DESC: 存儲桶名-可選
MANAGE_CONSTANT_S3_BUCKET_PLACEHOLDER: 英文逗號分隔例如bucket1,bucket2 MANAGE_CONSTANT_S3_BUCKET_PLACEHOLDER: 英文逗號分隔例如bucket1,bucket2
MANAGE_CONSTANT_S3_BASE_DIR_DESC: 起始目錄-可選 MANAGE_CONSTANT_S3_BASE_DIR_DESC: 起始目錄-可選
MANAGE_CONSTANT_S3_BASE_DIR_PLACEHOLDER: '英文逗號分隔,例如:/dir1,/dir2' MANAGE_CONSTANT_S3_BASE_DIR_PLACEHOLDER: '英文逗號分隔,例如:/dir1,/dir2'
MANAGE_CONSTANT_S3_DOGE_CLOUD_SUPPORT_DESC: 啟用 Doge Cloud 支援
MANAGE_CONSTANT_S3_DOGE_CLOUD_SUPPORT_TOOLTIP: 啟用後將會啟用Doge Cloud API
MANAGE_CONSTANT_S3_PAGING_DESC: 是否開啟分頁 MANAGE_CONSTANT_S3_PAGING_DESC: 是否開啟分頁
MANAGE_CONSTANT_S3_ITEMS_PAGE_DESC: 每頁顯示數量 MANAGE_CONSTANT_S3_ITEMS_PAGE_DESC: 每頁顯示數量
MANAGE_CONSTANT_S3_EXPLAIN: 存儲桶名和起始目錄配置時可通過英文逗號分隔不同存儲桶的設置,順序必須一致,逗號間留空或缺失項使用默認值 MANAGE_CONSTANT_S3_EXPLAIN: 存儲桶名和起始目錄配置時可通過英文逗號分隔不同存儲桶的設置,順序必須一致,逗號間留空或缺失項使用默認值

View File

@ -56,11 +56,18 @@ import path from 'path'
// 取消下载任务的加载文件列表、刷新下载文件传输列表 // 取消下载任务的加载文件列表、刷新下载文件传输列表
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static' import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static'
// dogecloudApi
import { dogecloudApi, DogecloudToken, getTempToken } from '../utils/dogeAPI'
class S3plistApi { class S3plistApi {
baseOptions: S3ClientConfig baseOptions: S3ClientConfig
logger: ManageLogger logger: ManageLogger
agent: any agent: any
proxy: string | undefined proxy: string | undefined
dogeCloudSupport: boolean
accessKeyId: string
secretAccessKey: string
bucketName: string
constructor ( constructor (
accessKeyId: string, accessKeyId: string,
@ -69,8 +76,14 @@ class S3plistApi {
sslEnabled: boolean, sslEnabled: boolean,
s3ForcePathStyle: boolean, s3ForcePathStyle: boolean,
proxy: string | undefined, proxy: string | undefined,
logger: ManageLogger logger: ManageLogger,
dogeCloudSupport: boolean = false,
bucketName: string = ''
) { ) {
this.accessKeyId = accessKeyId
this.secretAccessKey = secretAccessKey
this.dogeCloudSupport = dogeCloudSupport
this.bucketName = bucketName
this.baseOptions = { this.baseOptions = {
credentials: { credentials: {
accessKeyId, accessKeyId,
@ -85,6 +98,19 @@ class S3plistApi {
this.proxy = formatHttpProxy(proxy, 'string') as string | undefined this.proxy = formatHttpProxy(proxy, 'string') as string | undefined
} }
async getDogeCloudToken () {
if (!this.dogeCloudSupport) return
const token = await getTempToken(this.accessKeyId, this.secretAccessKey) as DogecloudToken
if (Object.keys(token).length === 0) {
throw new Error('manage.setting.dogeCloudTokenError')
}
this.baseOptions.credentials = {
accessKeyId: token.accessKeyId,
secretAccessKey: token.secretAccessKey,
sessionToken: token.sessionToken
}
}
setAgent (proxy: string | undefined, sslEnabled: boolean) : NodeHttpHandler { setAgent (proxy: string | undefined, sslEnabled: boolean) : NodeHttpHandler {
const agent = getAgent(proxy, sslEnabled) const agent = getAgent(proxy, sslEnabled)
const commonOptions: AgentOptions = { const commonOptions: AgentOptions = {
@ -149,6 +175,26 @@ class S3plistApi {
* *
*/ */
async getBucketList (): Promise<any> { async getBucketList (): Promise<any> {
if (this.dogeCloudSupport) {
try {
const res = await dogecloudApi('/oss/bucket/list.json', {}, false, this.accessKeyId, this.secretAccessKey)
for (const item of res.buckets) {
if (item.name === this.bucketName || item.s3Bucket === this.bucketName) {
return [
{
Name: item.s3Bucket,
CreationDate: item.ctime,
Location: item.region
}
]
}
}
return []
} catch (error) {
this.logParam(error, 'getBucketList')
}
return []
}
const options = Object.assign({}, this.baseOptions) as S3ClientConfig const options = Object.assign({}, this.baseOptions) as S3ClientConfig
const result: IStringKeyMap[] = [] const result: IStringKeyMap[] = []
const endpoint = options.endpoint as string || '' const endpoint = options.endpoint as string || ''
@ -216,7 +262,7 @@ class S3plistApi {
try { try {
do { do {
const options = Object.assign({}, this.baseOptions) as S3ClientConfig const options = Object.assign({}, this.baseOptions) as S3ClientConfig
options.region = region || 'us-east-1' options.region = String(region) || 'us-east-1'
const client = new S3Client(options) const client = new S3Client(options)
const command = new ListObjectsV2Command({ const command = new ListObjectsV2Command({
Bucket: bucket, Bucket: bucket,
@ -255,6 +301,7 @@ class S3plistApi {
async getBucketListBackstage (configMap: IStringKeyMap): Promise<any> { async getBucketListBackstage (configMap: IStringKeyMap): Promise<any> {
const window = windowManager.get(IWindowList.SETTING_WINDOW)! const window = windowManager.get(IWindowList.SETTING_WINDOW)!
const { bucketName: bucket, bucketConfig: { Location: region }, prefix, cancelToken } = configMap const { bucketName: bucket, bucketConfig: { Location: region }, prefix, cancelToken } = configMap
await this.getDogeCloudToken()
const slicedPrefix = prefix.slice(1) const slicedPrefix = prefix.slice(1)
const urlPrefix = configMap.customUrl || `https://${bucket}.s3.amazonaws.com` const urlPrefix = configMap.customUrl || `https://${bucket}.s3.amazonaws.com`
let marker let marker
@ -274,7 +321,7 @@ class S3plistApi {
try { try {
do { do {
const options = Object.assign({}, this.baseOptions) as S3ClientConfig const options = Object.assign({}, this.baseOptions) as S3ClientConfig
options.region = region || 'us-east-1' options.region = String(region) || 'us-east-1'
const client = new S3Client(options) const client = new S3Client(options)
const command = new ListObjectsV2Command({ const command = new ListObjectsV2Command({
Bucket: bucket, Bucket: bucket,
@ -316,6 +363,7 @@ class S3plistApi {
async getBucketFileList (configMap: IStringKeyMap): Promise<any> { async getBucketFileList (configMap: IStringKeyMap): Promise<any> {
const { bucketName: bucket, bucketConfig: { Location: region }, prefix, marker, itemsPerPage } = configMap const { bucketName: bucket, bucketConfig: { Location: region }, prefix, marker, itemsPerPage } = configMap
await this.getDogeCloudToken()
const slicedPrefix = prefix.slice(1) const slicedPrefix = prefix.slice(1)
const urlPrefix = configMap.customUrl || `https://${bucket}.s3.amazonaws.com` const urlPrefix = configMap.customUrl || `https://${bucket}.s3.amazonaws.com`
const result = { const result = {
@ -325,7 +373,7 @@ class S3plistApi {
success: false success: false
} }
try { try {
const options = Object.assign({}, { ...this.baseOptions, region: region || 'us-east-1' }) as S3ClientConfig const options = Object.assign({}, { ...this.baseOptions, region: String(region) || 'us-east-1' }) as S3ClientConfig
const client = new S3Client(options) const client = new S3Client(options)
const command = new ListObjectsV2Command({ const command = new ListObjectsV2Command({
Bucket: bucket, Bucket: bucket,
@ -362,9 +410,10 @@ class S3plistApi {
*/ */
async renameBucketFile (configMap: IStringKeyMap): Promise<boolean> { async renameBucketFile (configMap: IStringKeyMap): Promise<boolean> {
const { bucketName, region, oldKey, newKey } = configMap const { bucketName, region, oldKey, newKey } = configMap
await this.getDogeCloudToken()
let result = false let result = false
try { try {
const options = Object.assign({}, { ...this.baseOptions, region: region || 'us-east-1' }) as S3ClientConfig const options = Object.assign({}, { ...this.baseOptions, region: String(region) || 'us-east-1' }) as S3ClientConfig
const client = new S3Client(options) const client = new S3Client(options)
const command = new CopyObjectCommand({ const command = new CopyObjectCommand({
Bucket: bucketName, Bucket: bucketName,
@ -403,10 +452,11 @@ class S3plistApi {
*/ */
async deleteBucketFile (configMap: IStringKeyMap): Promise<boolean> { async deleteBucketFile (configMap: IStringKeyMap): Promise<boolean> {
const { bucketName, region, key } = configMap const { bucketName, region, key } = configMap
await this.getDogeCloudToken()
let result = false let result = false
try { try {
const options = Object.assign({}, this.baseOptions) as S3ClientConfig const options = Object.assign({}, this.baseOptions) as S3ClientConfig
options.region = region || 'us-east-1' options.region = String(region) || 'us-east-1'
const client = new S3Client(options) const client = new S3Client(options)
const command = new DeleteObjectCommand({ const command = new DeleteObjectCommand({
Bucket: bucketName, Bucket: bucketName,
@ -430,6 +480,7 @@ class S3plistApi {
*/ */
async deleteBucketFolder (configMap: IStringKeyMap): Promise<boolean> { async deleteBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
const { bucketName, region, key } = configMap const { bucketName, region, key } = configMap
await this.getDogeCloudToken()
let marker let marker
let result = false let result = false
let IsTruncated let IsTruncated
@ -441,7 +492,7 @@ class S3plistApi {
try { try {
do { do {
const options = Object.assign({}, this.baseOptions) as S3ClientConfig const options = Object.assign({}, this.baseOptions) as S3ClientConfig
options.region = region || 'us-east-1' options.region = String(region) || 'us-east-1'
const client = new S3Client(options) const client = new S3Client(options)
const command = new ListObjectsV2Command({ const command = new ListObjectsV2Command({
Bucket: bucketName, Bucket: bucketName,
@ -476,7 +527,7 @@ class S3plistApi {
if (allFileList.Contents.length > 0) { if (allFileList.Contents.length > 0) {
const cycle = Math.ceil(allFileList.Contents.length / 1000) const cycle = Math.ceil(allFileList.Contents.length / 1000)
const options = Object.assign({}, this.baseOptions) as S3ClientConfig const options = Object.assign({}, this.baseOptions) as S3ClientConfig
options.region = region || 'us-east-1' options.region = String(region) || 'us-east-1'
const client = new S3Client(options) const client = new S3Client(options)
for (let i = 0; i < cycle; i++) { for (let i = 0; i < cycle; i++) {
const deleteList = allFileList.Contents.slice(i * 1000, (i + 1) * 1000) const deleteList = allFileList.Contents.slice(i * 1000, (i + 1) * 1000)
@ -518,9 +569,10 @@ class S3plistApi {
*/ */
async getPreSignedUrl (configMap: IStringKeyMap): Promise<string> { async getPreSignedUrl (configMap: IStringKeyMap): Promise<string> {
const { bucketName, region, key, expires } = configMap const { bucketName, region, key, expires } = configMap
await this.getDogeCloudToken()
try { try {
const options = Object.assign({}, this.baseOptions) as S3ClientConfig const options = Object.assign({}, this.baseOptions) as S3ClientConfig
options.region = region || 'us-east-1' options.region = String(region) || 'us-east-1'
const client = new S3Client(options) const client = new S3Client(options)
const signedUrl = await getSignedUrl(client, new GetObjectCommand({ const signedUrl = await getSignedUrl(client, new GetObjectCommand({
Bucket: bucketName, Bucket: bucketName,
@ -541,10 +593,11 @@ class S3plistApi {
*/ */
async createBucketFolder (configMap: IStringKeyMap): Promise<boolean> { async createBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
const { bucketName, region, key } = configMap const { bucketName, region, key } = configMap
await this.getDogeCloudToken()
let result = false let result = false
try { try {
const options = Object.assign({}, this.baseOptions) as S3ClientConfig const options = Object.assign({}, this.baseOptions) as S3ClientConfig
options.region = region || 'us-east-1' options.region = String(region) || 'us-east-1'
const client = new S3Client(options) const client = new S3Client(options)
const command = new PutObjectCommand({ const command = new PutObjectCommand({
Bucket: bucketName, Bucket: bucketName,
@ -582,10 +635,11 @@ class S3plistApi {
const allowedAcl = ['private', 'public-read', 'public-read-write', 'aws-exec-read', 'authenticated-read', 'bucket-owner-read', 'bucket-owner-full-control'] const allowedAcl = ['private', 'public-read', 'public-read-write', 'aws-exec-read', 'authenticated-read', 'bucket-owner-read', 'bucket-owner-full-control']
for (const item of fileArray) { for (const item of fileArray) {
const { bucketName, region, key, filePath, fileName, aclForUpload } = item const { bucketName, region, key, filePath, fileName, aclForUpload } = item
await this.getDogeCloudToken()
const options = Object.assign({}, this.baseOptions) as S3ClientConfig const options = Object.assign({}, this.baseOptions) as S3ClientConfig
options.region = region || 'us-east-1' options.region = String(region) || 'us-east-1'
const client = new S3Client(options) const client = new S3Client(options)
const id = `${bucketName}-${region}-${key}-${filePath}` const id = `${bucketName}-${String(region)}-${key}-${filePath}`
if (instance.getUploadTask(id)) { if (instance.getUploadTask(id)) {
continue continue
} }
@ -598,7 +652,7 @@ class S3plistApi {
sourceFilePath: filePath, sourceFilePath: filePath,
targetFilePath: key, targetFilePath: key,
targetFileBucket: bucketName, targetFileBucket: bucketName,
targetFileRegion: region targetFileRegion: String(region)
}) })
const parallelUploads3 = new Upload({ const parallelUploads3 = new Upload({
client, client,
@ -661,7 +715,7 @@ class S3plistApi {
for (const item of fileArray) { for (const item of fileArray) {
const { bucketName, region, key, fileName, customUrl } = item const { bucketName, region, key, fileName, customUrl } = item
const savedFilePath = path.join(downloadPath, fileName) const savedFilePath = path.join(downloadPath, fileName)
const id = `${bucketName}-${region}-${key}-${savedFilePath}` const id = `${bucketName}-${String(region)}-${key}-${savedFilePath}`
if (instance.getDownloadTask(id)) { if (instance.getDownloadTask(id)) {
continue continue
} }
@ -674,7 +728,7 @@ class S3plistApi {
}) })
const preSignedUrl = await this.getPreSignedUrl({ const preSignedUrl = await this.getPreSignedUrl({
bucketName, bucketName,
region, region: String(region),
key, key,
expires: 36000, expires: 36000,
customUrl customUrl

View File

@ -67,7 +67,7 @@ export class ManageApi extends EventEmitter implements ManageApiType {
case 'smms': case 'smms':
return new API.SmmsApi(this.currentPicBedConfig.token, this.logger) return new API.SmmsApi(this.currentPicBedConfig.token, this.logger)
case 's3plist': case 's3plist':
return new API.S3plistApi(this.currentPicBedConfig.accessKeyId, this.currentPicBedConfig.secretAccessKey, this.currentPicBedConfig.endpoint, this.currentPicBedConfig.sslEnabled, this.currentPicBedConfig.s3ForcePathStyle, this.currentPicBedConfig.proxy, this.logger) return new API.S3plistApi(this.currentPicBedConfig.accessKeyId, this.currentPicBedConfig.secretAccessKey, this.currentPicBedConfig.endpoint, this.currentPicBedConfig.sslEnabled, this.currentPicBedConfig.s3ForcePathStyle, this.currentPicBedConfig.proxy, this.logger, this.currentPicBedConfig.dogeCloudSupport || false, this.currentPicBedConfig.bucketName || '')
case 'sftp': case 'sftp':
return new API.SftpApi(this.currentPicBedConfig.host, this.currentPicBedConfig.port, this.currentPicBedConfig.username, this.currentPicBedConfig.password, this.currentPicBedConfig.privateKey, this.currentPicBedConfig.passphrase, this.currentPicBedConfig.fileMode, this.currentPicBedConfig.dirMode, this.logger) return new API.SftpApi(this.currentPicBedConfig.host, this.currentPicBedConfig.port, this.currentPicBedConfig.username, this.currentPicBedConfig.password, this.currentPicBedConfig.privateKey, this.currentPicBedConfig.passphrase, this.currentPicBedConfig.fileMode, this.currentPicBedConfig.dirMode, this.logger)
case 'tcyun': case 'tcyun':

View File

@ -790,6 +790,7 @@ async function transUpToManage (config: IUploaderConfigListItem, picBedName: str
sslEnabled: config.endpoint ? config.endpoint.startsWith('https') : false, sslEnabled: config.endpoint ? config.endpoint.startsWith('https') : false,
aclForUpload: 'public-read', aclForUpload: 'public-read',
s3ForcePathStyle: config.pathStyleAccess, s3ForcePathStyle: config.pathStyleAccess,
dogeCloudSupport: false,
transformedConfig: JSON.stringify( transformedConfig: JSON.stringify(
config.urlPrefix config.urlPrefix
? { ? {

View File

@ -642,6 +642,13 @@ export const supportedPicBedList: IStringKeyMap = {
default: '/', default: '/',
tooltip: baseDirTooltip tooltip: baseDirTooltip
}, },
dogeCloudSupport: {
required: false,
description: $T('MANAGE_CONSTANT_S3_DOGE_CLOUD_SUPPORT_DESC'),
default: false,
type: 'boolean',
tooltip: $T('MANAGE_CONSTANT_S3_DOGE_CLOUD_SUPPORT_TOOLTIP')
},
paging: { paging: {
required: true, required: true,
description: $T('MANAGE_CONSTANT_S3_PAGING_DESC'), description: $T('MANAGE_CONSTANT_S3_PAGING_DESC'),
@ -659,7 +666,7 @@ export const supportedPicBedList: IStringKeyMap = {
} }
}, },
explain: $T('MANAGE_CONSTANT_S3_EXPLAIN'), explain: $T('MANAGE_CONSTANT_S3_EXPLAIN'),
options: ['alias', 'accessKeyId', 'secretAccessKey', 'endpoint', 'sslEnabled', 's3ForcePathStyle', 'proxy', 'aclForUpload', 'bucketName', 'baseDir', 'paging', 'itemsPerPage'], options: ['alias', 'accessKeyId', 'secretAccessKey', 'endpoint', 'sslEnabled', 's3ForcePathStyle', 'proxy', 'aclForUpload', 'bucketName', 'baseDir', 'dogeCloudSupport', 'paging', 'itemsPerPage'],
refLink: 'https://github.com/wayjam/picgo-plugin-s3', refLink: 'https://github.com/wayjam/picgo-plugin-s3',
referenceText: $T('MANAGE_CONSTANT_S3_REFER_TEXT') referenceText: $T('MANAGE_CONSTANT_S3_REFER_TEXT')
}, },

View File

@ -539,6 +539,8 @@ interface ILocales {
MANAGE_CONSTANT_S3_BUCKET_PLACEHOLDER: string MANAGE_CONSTANT_S3_BUCKET_PLACEHOLDER: string
MANAGE_CONSTANT_S3_BASE_DIR_DESC: string MANAGE_CONSTANT_S3_BASE_DIR_DESC: string
MANAGE_CONSTANT_S3_BASE_DIR_PLACEHOLDER: string MANAGE_CONSTANT_S3_BASE_DIR_PLACEHOLDER: string
MANAGE_CONSTANT_S3_DOGE_CLOUD_SUPPORT_DESC: string
MANAGE_CONSTANT_S3_DOGE_CLOUD_SUPPORT_TOOLTIP: string
MANAGE_CONSTANT_S3_PAGING_DESC: string MANAGE_CONSTANT_S3_PAGING_DESC: string
MANAGE_CONSTANT_S3_ITEMS_PAGE_DESC: string MANAGE_CONSTANT_S3_ITEMS_PAGE_DESC: string
MANAGE_CONSTANT_S3_EXPLAIN: string MANAGE_CONSTANT_S3_EXPLAIN: string