feat(smarts3-server): Introduce native custom S3 server implementation (Smarts3Server) with routing, middleware, context, filesystem store, controllers and XML utilities; add SmartXml and AWS SDK test; keep optional legacy s3rver backend.
This commit is contained in:
145
ts/classes/s3-error.ts
Normal file
145
ts/classes/s3-error.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
/**
|
||||
* S3 error codes mapped to HTTP status codes
|
||||
*/
|
||||
const S3_ERROR_CODES: Record<string, number> = {
|
||||
'AccessDenied': 403,
|
||||
'BadDigest': 400,
|
||||
'BadRequest': 400,
|
||||
'BucketAlreadyExists': 409,
|
||||
'BucketAlreadyOwnedByYou': 409,
|
||||
'BucketNotEmpty': 409,
|
||||
'CredentialsNotSupported': 400,
|
||||
'EntityTooSmall': 400,
|
||||
'EntityTooLarge': 400,
|
||||
'ExpiredToken': 400,
|
||||
'IncompleteBody': 400,
|
||||
'IncorrectNumberOfFilesInPostRequest': 400,
|
||||
'InlineDataTooLarge': 400,
|
||||
'InternalError': 500,
|
||||
'InvalidArgument': 400,
|
||||
'InvalidBucketName': 400,
|
||||
'InvalidDigest': 400,
|
||||
'InvalidLocationConstraint': 400,
|
||||
'InvalidPart': 400,
|
||||
'InvalidPartOrder': 400,
|
||||
'InvalidRange': 416,
|
||||
'InvalidRequest': 400,
|
||||
'InvalidSecurity': 403,
|
||||
'InvalidSOAPRequest': 400,
|
||||
'InvalidStorageClass': 400,
|
||||
'InvalidTargetBucketForLogging': 400,
|
||||
'InvalidToken': 400,
|
||||
'InvalidURI': 400,
|
||||
'KeyTooLongError': 400,
|
||||
'MalformedACLError': 400,
|
||||
'MalformedPOSTRequest': 400,
|
||||
'MalformedXML': 400,
|
||||
'MaxMessageLengthExceeded': 400,
|
||||
'MaxPostPreDataLengthExceededError': 400,
|
||||
'MetadataTooLarge': 400,
|
||||
'MethodNotAllowed': 405,
|
||||
'MissingContentLength': 411,
|
||||
'MissingRequestBodyError': 400,
|
||||
'MissingSecurityElement': 400,
|
||||
'MissingSecurityHeader': 400,
|
||||
'NoLoggingStatusForKey': 400,
|
||||
'NoSuchBucket': 404,
|
||||
'NoSuchKey': 404,
|
||||
'NoSuchLifecycleConfiguration': 404,
|
||||
'NoSuchUpload': 404,
|
||||
'NoSuchVersion': 404,
|
||||
'NotImplemented': 501,
|
||||
'NotSignedUp': 403,
|
||||
'OperationAborted': 409,
|
||||
'PermanentRedirect': 301,
|
||||
'PreconditionFailed': 412,
|
||||
'Redirect': 307,
|
||||
'RequestIsNotMultiPartContent': 400,
|
||||
'RequestTimeout': 400,
|
||||
'RequestTimeTooSkewed': 403,
|
||||
'RequestTorrentOfBucketError': 400,
|
||||
'SignatureDoesNotMatch': 403,
|
||||
'ServiceUnavailable': 503,
|
||||
'SlowDown': 503,
|
||||
'TemporaryRedirect': 307,
|
||||
'TokenRefreshRequired': 400,
|
||||
'TooManyBuckets': 400,
|
||||
'UnexpectedContent': 400,
|
||||
'UnresolvableGrantByEmailAddress': 400,
|
||||
'UserKeyMustBeSpecified': 400,
|
||||
};
|
||||
|
||||
/**
|
||||
* S3-compatible error class that formats errors as XML responses
|
||||
*/
|
||||
export class S3Error extends Error {
|
||||
public status: number;
|
||||
public code: string;
|
||||
public detail: Record<string, any>;
|
||||
|
||||
constructor(
|
||||
code: string,
|
||||
message: string,
|
||||
detail: Record<string, any> = {}
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'S3Error';
|
||||
this.code = code;
|
||||
this.status = S3_ERROR_CODES[code] || 500;
|
||||
this.detail = detail;
|
||||
|
||||
// Maintain proper stack trace
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, S3Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert error to S3-compatible XML format
|
||||
*/
|
||||
public toXML(): string {
|
||||
const smartXmlInstance = new plugins.SmartXml();
|
||||
const errorObj: any = {
|
||||
Error: {
|
||||
Code: this.code,
|
||||
Message: this.message,
|
||||
...this.detail,
|
||||
},
|
||||
};
|
||||
|
||||
const xml = smartXmlInstance.createXmlFromObject(errorObj);
|
||||
|
||||
// Ensure XML declaration
|
||||
if (!xml.startsWith('<?xml')) {
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>\n${xml}`;
|
||||
}
|
||||
|
||||
return xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create S3Error from a generic Error
|
||||
*/
|
||||
public static fromError(err: any): S3Error {
|
||||
if (err instanceof S3Error) {
|
||||
return err;
|
||||
}
|
||||
|
||||
// Map common errors
|
||||
if (err.code === 'ENOENT') {
|
||||
return new S3Error('NoSuchKey', 'The specified key does not exist.');
|
||||
}
|
||||
if (err.code === 'EACCES') {
|
||||
return new S3Error('AccessDenied', 'Access Denied');
|
||||
}
|
||||
|
||||
// Default to internal error
|
||||
return new S3Error(
|
||||
'InternalError',
|
||||
'We encountered an internal error. Please try again.',
|
||||
{ OriginalError: err.message }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user