import { BOOLEAN , DATE , INTEGER , literal , Model , Op , Options as SequelizeOptions , Sequelize , TEXT , Transaction } from "sequelize" ; import uuid from "uuid/v4" ; import { DEFAULT_SQL_CHARSET , DEFAULT_SQL_COLLATE } from "../../common/utils/constants" ; import { convertDateTimeStringMsTo7Digital } from "../../common/utils/utils" ; import { newEtag } from "../../common/utils/utils" ; import { validateReadConditions } from "../conditions/ReadConditionalHeadersValidator" ; import { validateWriteConditions } from "../conditions/WriteConditionalHeadersValidator" ; import StorageErrorFactory from "../errors/StorageErrorFactory" ; import * as Models from "../generated/artifacts/models" ; import Context from "../generated/Context" ; import BlobLeaseAdapter from "../lease/BlobLeaseAdapter" ; import BlobLeaseSyncer from "../lease/BlobLeaseSyncer" ; import BlobReadLeaseValidator from "../lease/BlobReadLeaseValidator" ; import BlobWriteLeaseSyncer from "../lease/BlobWriteLeaseSyncer" ; import BlobWriteLeaseValidator from "../lease/BlobWriteLeaseValidator" ; import ContainerDeleteLeaseValidator from "../lease/ContainerDeleteLeaseValidator" ; import ContainerLeaseAdapter from "../lease/ContainerLeaseAdapter" ; import ContainerLeaseSyncer from "../lease/ContainerLeaseSyncer" ; import ContainerReadLeaseValidator from "../lease/ContainerReadLeaseValidator" ; import { ILease } from "../lease/ILeaseState" ; import LeaseFactory from "../lease/LeaseFactory" ; import { DEFAULT_LIST_BLOBS_MAX_RESULTS , DEFAULT_LIST_CONTAINERS_MAX_RESULTS } from "../utils/constants" ; import BlobReferredExtentsAsyncIterator from "./BlobReferredExtentsAsyncIterator" ; import IBlobMetadataStore , { AcquireBlobLeaseResponse , AcquireContainerLeaseResponse , BlobId , BlobModel , BlockModel , BreakBlobLeaseResponse , BreakContainerLeaseResponse , ChangeBlobLeaseResponse , ChangeContainerLeaseResponse , ContainerModel , CreateSnapshotResponse , GetBlobPropertiesRes , GetContainerAccessPolicyResponse , GetContainerPropertiesResponse , GetPageRangeResponse , IContainerMetadata , IExtentChunk , PersistencyBlockModel , ReleaseBlobLeaseResponse , RenewBlobLeaseResponse , RenewContainerLeaseResponse , ServicePropertiesModel , SetContainerAccessPolicyOptions } from "./IBlobMetadataStore" ; class ServicesModel extends Model { } class ContainersModel extends Model { } class BlobsModel extends Model { } class BlocksModel extends Model { } interface IBlobContentProperties { contentLength ? : number ; contentType ? : string ; contentEncoding ? : string ; contentLanguage ? : string ; contentMD5 ? : Uint8Array ; contentDisposition ? : string ; cacheControl ? : string ; } export class implements IBlobMetadataStore export default class SqlBlobMetadataStore implements IBlobMetadataStore { private initialized : boolean = false ; private closed : boolean = false ; private readonly sequelize : Sequelize ; param { string } connectionURI param { SequelizeOptions } sequelizeOptions memberof public constructor ( connectionURI , sequelizeOptions ? ) { if ( connectionURI . startsWith ( "mssql" ) && sequelizeOptions ) { sequelizeOptions . dialectOptions = sequelizeOptions . dialectOptions || { } ; ( sequelizeOptions . dialectOptions as any ) . options = ( sequelizeOptions . dialectOptions as any ) . options || { } ; ( sequelizeOptions . dialectOptions as any ) . options . encrypt = true ; } this . sequelize = new Sequelize ( connectionURI , sequelizeOptions ) ; } public async init ( ) { await this . sequelize . authenticate ( ) ; ServicesModel . init ( { accountName : { type : "VARCHAR(32)" , primaryKey : true } , defaultServiceVersion : { type : "VARCHAR(10)" } , cors : { type : "VARCHAR(4095)" } , logging : { type : "VARCHAR(255)" } , minuteMetrics : { type : "VARCHAR(255)" } , hourMetrics : { type : "VARCHAR(255)" } , staticWebsite : { type : "VARCHAR(1023)" } , deleteRetentionPolicy : { type : "VARCHAR(255)" } } , { sequelize : this . sequelize , modelName : "Services" , tableName : "Services" , timestamps : false } ) ; ContainersModel . init ( { accountName : { type : "VARCHAR(32)" , unique : "accountname_containername" } , containerName : { type : "VARCHAR(63)" , unique : "accountname_containername" } , containerId : { type : INTEGER . UNSIGNED , primaryKey : true , autoIncrement : true } , lastModified : { allowNull : false , type : DATE ( 6 ) } , etag : { allowNull : false , type : "VARCHAR(127)" } , metadata : { type : "VARCHAR(4095)" } , containerAcl : { type : "VARCHAR(1023)" } , publicAccess : { type : "VARCHAR(31)" } , lease : { type : "VARCHAR(1023)" } , hasImmutabilityPolicy : { type : BOOLEAN } , hasLegalHold : { type : BOOLEAN } } , { sequelize : this . sequelize , modelName : "Containers" , tableName : "Containers" , charset : DEFAULT_SQL_CHARSET , collate : DEFAULT_SQL_COLLATE , timestamps : false } ) ; BlobsModel . init ( { accountName : { type : "VARCHAR(64)" , allowNull : false } , containerName : { type : "VARCHAR(255)" , allowNull : false } , blobName : { type : "VARCHAR(255)" , allowNull : false } , snapshot : { type : "VARCHAR(64)" , allowNull : false , defaultValue : "" } , blobId : { type : INTEGER . UNSIGNED , primaryKey : true , autoIncrement : true } , lastModified : { allowNull : false , type : DATE ( 6 ) } , creationTime : { allowNull : false , type : DATE ( 6 ) } , accessTierChangeTime : { allowNull : true , type : DATE ( 6 ) } , accessTierInferred : { type : BOOLEAN } , etag : { allowNull : false , type : "VARCHAR(127)" } , blobType : { allowNull : false , type : "VARCHAR(31)" } , blobSequenceNumber : { type : "VARCHAR(63)" } , accessTier : { type : "VARCHAR(31)" } , contentProperties : { type : "VARCHAR(1023)" } , lease : { type : "VARCHAR(1023)" } , deleting : { type : INTEGER . UNSIGNED , defaultValue : 0 , allowNull : false } , isCommitted : { type : BOOLEAN , allowNull : false } , persistency : { type : "VARCHAR(255)" } , committedBlocksInOrder : { type : TEXT ( { length : "medium" } ) } , metadata : { type : "VARCHAR(2047)" } } , { sequelize : this . sequelize , modelName : "Blobs" , tableName : "Blobs" , timestamps : false , charset : DEFAULT_SQL_CHARSET , collate : DEFAULT_SQL_COLLATE , indexes : [ { unique : true , fields : [ "accountName" , "containerName" , "blobName" , "snapshot" , "deleting" ] } ] } ) ; BlocksModel . init ( { accountName : { type : "VARCHAR(64)" , allowNull : false } , containerName : { type : "VARCHAR(255)" , allowNull : false } , blobName : { type : "VARCHAR(255)" , allowNull : false } , blockName : { type : "VARCHAR(64)" , allowNull : false } , deleting : { type : INTEGER . UNSIGNED , defaultValue : 0 , allowNull : false } , size : { type : INTEGER . UNSIGNED , allowNull : false } , persistency : { type : "VARCHAR(255)" } } , { sequelize : this . sequelize , modelName : "Blocks" , tableName : "Blocks" , timestamps : false , indexes : [ { unique : true , fields : [ "accountName" , "containerName" , "blobName" , "blockName" ] } ] } ) ; await this . sequelize . sync ( ) ; this . initialized = true ; } public isInitialized ( ) { return this . initialized ; } public async close ( ) { await this . sequelize . close ( ) ; this . closed = true ; } public isClosed ( ) { return this . closed ; } public async clean ( ) { } public async setServiceProperties ( context , serviceProperties ) { return this . sequelize . transaction ( async ( t ) => { const findResult = await ServicesModel . findByPk ( serviceProperties . accountName , { transaction : t } ) ; const updateValues = { defaultServiceVersion : serviceProperties . defaultServiceVersion , cors : this . serializeModelValue ( serviceProperties . cors ) , logging : this . serializeModelValue ( serviceProperties . logging ) , minuteMetrics : this . serializeModelValue ( serviceProperties . minuteMetrics ) , hourMetrics : this . serializeModelValue ( serviceProperties . hourMetrics ) , staticWebsite : this . serializeModelValue ( serviceProperties . staticWebsite ) , deleteRetentionPolicy : this . serializeModelValue ( serviceProperties . deleteRetentionPolicy ) } ; if ( findResult === null ) { await ServicesModel . create ( { accountName : serviceProperties . accountName , ... updateValues } , { transaction : t } ) ; } else { const updateResult = await ServicesModel . update ( updateValues , { transaction : t , where : { accountName : serviceProperties . accountName } } ) ; const updatedRows = updateResult [ 0 ] ; if ( updatedRows > 1 ) { throw Error ( `SqlBlobMetadataStore:updateServiceProperties() failed. Update operation affect ${ updatedRows } rows.` ) ; } } return serviceProperties ; } ) ; } public async getServiceProperties ( context , account ) { const findResult = await ServicesModel . findByPk ( account ) ; if ( findResult === null ) { return undefined ; } const logging = this . deserializeModelValue ( findResult , "logging" ) ; const hourMetrics = this . deserializeModelValue ( findResult , "hourMetrics" ) ; const minuteMetrics = this . deserializeModelValue ( findResult , "minuteMetrics" ) ; const cors = this . deserializeModelValue ( findResult , "cors" ) ; const deleteRetentionPolicy = this . deserializeModelValue ( findResult , "deleteRetentionPolicy" ) ; const staticWebsite = this . deserializeModelValue ( findResult , "staticWebsite" ) ; const defaultServiceVersion = this . getModelValue < string > ( findResult , "defaultServiceVersion" ) ; const ret = { accountName : account } ; if ( logging !== undefined ) { ret . logging = logging ; } if ( hourMetrics !== undefined ) { ret . hourMetrics = hourMetrics ; } if ( minuteMetrics !== undefined ) { ret . minuteMetrics = minuteMetrics ; } if ( cors !== undefined ) { ret . cors = cors ; } if ( deleteRetentionPolicy !== undefined ) { ret . deleteRetentionPolicy = deleteRetentionPolicy ; } if ( staticWebsite !== undefined ) { ret . staticWebsite = staticWebsite ; } if ( defaultServiceVersion !== undefined ) { ret . defaultServiceVersion = defaultServiceVersion ; } return ret ; } public async listContainers ( context , account , prefix = "" , maxResults = DEFAULT_LIST_CONTAINERS_MAX_RESULTS , marker ) { const whereQuery = { accountName : account } ; if ( prefix . length > 0 ) { whereQuery . containerName = { [ Op . like ] : `${ prefix }%` } ; } if ( marker !== "" ) { if ( whereQuery . containerName === undefined ) { whereQuery . containerName = { [ Op . gt ] : marker } ; } else { whereQuery . containerName [ Op . gt ] = marker ; } } const findResult = await ContainersModel . findAll ( { limit : maxResults + 1 , where : whereQuery as any , order : [ [ "containerName" , "ASC" ] ] } ) ; const leaseUpdateMapper = ( model ) => { const containerModel = this . convertDbModelToContainerModel ( model ) ; return LeaseFactory . createLeaseState ( new ContainerLeaseAdapter ( containerModel ) , context ) . sync ( new ContainerLeaseSyncer ( containerModel ) ) ; } ; if ( findResult . length <= maxResults ) { return [ findResult . map ( leaseUpdateMapper ) , undefined ] ; } else { const tail = findResult [ findResult . length - 2 ] ; findResult . pop ( ) ; const nextMarker = this . getModelValue < string > ( tail , "containerName" , true ) ; return [ findResult . map ( leaseUpdateMapper ) , nextMarker ] ; } } public async createContainer ( context , container ) { try { await ContainersModel . create ( this . convertContainerModelToDbModel ( container ) ) ; return container ; } catch ( err ) { if ( err . name === "SequelizeUniqueConstraintError" ) { throw StorageErrorFactory . getContainerAlreadyExists ( context . contextId ) ; } throw err ; } } public async getContainerProperties ( context , account , container , leaseAccessConditions ? ) { const findResult = await this . assertContainerExists ( context , account , container , undefined , true ) ; const containerModel = this . convertDbModelToContainerModel ( findResult ) ; return LeaseFactory . createLeaseState ( new ContainerLeaseAdapter ( containerModel ) , context ) . validate ( new ContainerReadLeaseValidator ( leaseAccessConditions ) ) . sync ( new ContainerLeaseSyncer ( containerModel ) ) ; } public async deleteContainer ( context , account , container , options = { } ) { await this . sequelize . transaction ( async ( t ) => { const findResult = await ContainersModel . findOne ( { attributes : [ "accountName" , "containerName" , "etag" , "lastModified" , "lease" ] , where : { accountName : account , containerName : container } , transaction : t } ) ; validateWriteConditions ( context , options . modifiedAccessConditions , findResult ? this . convertDbModelToContainerModel ( findResult ) : undefined ) ; if ( findResult === null || findResult === undefined ) { throw StorageErrorFactory . getContainerNotFound ( context . contextId ) ; } LeaseFactory . createLeaseState ( this . convertDbModelToLease ( findResult ) , context ) . validate ( new ContainerDeleteLeaseValidator ( options . leaseAccessConditions ) ) ; await ContainersModel . destroy ( { where : { accountName : account , containerName : container } , transaction : t } ) ; await BlobsModel . update ( { deleting : literal ( "deleting + 1" ) } , { where : { accountName : account , containerName : container } , transaction : t } ) ; await BlocksModel . update ( { deleting : literal ( "deleting + 1" ) } , { where : { accountName : account , containerName : container } , transaction : t } ) ; } ) ; } public async setContainerMetadata ( context , account , container , lastModified , etag , metadata ? , leaseAccessConditions ? , modifiedAccessConditions ? ) { return this . sequelize . transaction ( async ( t ) => { const findResult = await ContainersModel . findOne ( { attributes : [ "accountName" , "containerName" , "etag" , "lastModified" , "lease" ] , where : { accountName : account , containerName : container } , transaction : t } ) ; validateWriteConditions ( context , modifiedAccessConditions , findResult ? this . convertDbModelToContainerModel ( findResult ) : undefined ) ; if ( findResult === null || findResult === undefined ) { throw StorageErrorFactory . getContainerNotFound ( context . contextId ) ; } LeaseFactory . createLeaseState ( this . convertDbModelToLease ( findResult ) , context ) . validate ( new ContainerReadLeaseValidator ( leaseAccessConditions ) ) ; await ContainersModel . update ( { lastModified , etag , metadata : this . serializeModelValue ( metadata ) || null } , { where : { accountName : account , containerName : container } , transaction : t } ) ; } ) ; } public async getContainerACL ( context , account , container , leaseAccessConditions ? ) { const findResult = await ContainersModel . findOne ( { where : { accountName : account , containerName : container } } ) ; if ( findResult === null || findResult === undefined ) { throw StorageErrorFactory . getContainerNotFound ( context . contextId ) ; } const containerModel = this . convertDbModelToContainerModel ( findResult ) ; LeaseFactory . createLeaseState ( new ContainerLeaseAdapter ( containerModel ) , context ) . validate ( new ContainerReadLeaseValidator ( leaseAccessConditions ) ) . sync ( new ContainerLeaseSyncer ( containerModel ) ) ; return { properties : containerModel . properties , containerAcl : containerModel . containerAcl } ; } public async setContainerACL ( context , account , container , setAclModel ) { await this . sequelize . transaction ( async ( t ) => { const findResult = await ContainersModel . findOne ( { attributes : [ "accountName" , "containerName" , "etag" , "lastModified" , "lease" ] , where : { accountName : account , containerName : container } , transaction : t } ) ; validateWriteConditions ( context , setAclModel . modifiedAccessConditions , findResult ? this . convertDbModelToContainerModel ( findResult ) : undefined ) ; if ( findResult === null || findResult === undefined ) { throw StorageErrorFactory . getContainerNotFound ( context . contextId ) ; } const lease = this . convertDbModelToLease ( findResult ) ; LeaseFactory . createLeaseState ( lease , context ) . validate ( new ContainerReadLeaseValidator ( setAclModel . leaseAccessConditions ) ) ; const updateResult = await ContainersModel . update ( { lastModified : setAclModel . lastModified , etag : setAclModel . etag , containerAcl : this . serializeModelValue ( setAclModel . containerAcl ) || null , publicAccess : this . serializeModelValue ( setAclModel . publicAccess ) } , { where : { accountName : account , containerName : container } , transaction : t } ) ; if ( updateResult [ 0 ] === 0 ) { throw StorageErrorFactory . getContainerNotFound ( context . contextId ) ; } } ) ; } public async acquireContainerLease ( context , account , container , options ) { return this . sequelize . transaction ( async ( t ) => { const findResult = await ContainersModel . findOne ( { where : { accountName : account , containerName : container } , transaction : t } ) ; validateWriteConditions ( context , options . modifiedAccessConditions , findResult ? this . convertDbModelToContainerModel ( findResult ) : undefined ) ; if ( findResult === null || findResult === undefined ) { throw StorageErrorFactory . getContainerNotFound ( context . contextId ) ; } const containerModel = this . convertDbModelToContainerModel ( findResult ) ; LeaseFactory . createLeaseState ( new ContainerLeaseAdapter ( containerModel ) , context ) . acquire ( options . duration ! , options . proposedLeaseId ) . sync ( new ContainerLeaseSyncer ( containerModel ) ) ; await ContainersModel . update ( this . convertLeaseToDbModel ( new ContainerLeaseAdapter ( containerModel ) ) , { where : { accountName : account , containerName : container } , transaction : t } ) ; return { properties : containerModel . properties , leaseId : containerModel . leaseId } ; } ) ; } public async releaseContainerLease ( context , account , container , leaseId , options = { } ) { return this . sequelize . transaction ( async ( t ) => { const findResult = await ContainersModel . findOne ( { where : { accountName : account , containerName : container } , transaction : t } ) ; validateWriteConditions ( context , options . modifiedAccessConditions , findResult ? this . convertDbModelToContainerModel ( findResult ) : undefined ) ; if ( findResult === null || findResult === undefined ) { throw StorageErrorFactory . getContainerNotFound ( context . contextId ) ; } const containerModel = this . convertDbModelToContainerModel ( findResult ) ; LeaseFactory . createLeaseState ( new ContainerLeaseAdapter ( containerModel ) , context ) . release ( leaseId ) . sync ( new ContainerLeaseSyncer ( containerModel ) ) ; await ContainersModel . update ( this . convertLeaseToDbModel ( new ContainerLeaseAdapter ( containerModel ) ) , { where : { accountName : account , containerName : container } , transaction : t } ) ; return containerModel . properties ; } ) ; } public async renewContainerLease ( context , account , container , leaseId , options = { } ) { return this . sequelize . transaction ( async ( t ) => { const findResult = await ContainersModel . findOne ( { where : { accountName : account , containerName : container } , transaction : t } ) ; validateWriteConditions ( context , options . modifiedAccessConditions , findResult ? this . convertDbModelToContainerModel ( findResult ) : undefined ) ; if ( findResult === null || findResult === undefined ) { throw StorageErrorFactory . getContainerNotFound ( context . contextId ) ; } const containerModel = this . convertDbModelToContainerModel ( findResult ) ; LeaseFactory . createLeaseState ( new ContainerLeaseAdapter ( containerModel ) , context ) . renew ( leaseId ) . sync ( new ContainerLeaseSyncer ( containerModel ) ) ; await ContainersModel . update ( this . convertLeaseToDbModel ( new ContainerLeaseAdapter ( containerModel ) ) , { where : { accountName : account , containerName : container } , transaction : t } ) ; return { properties : containerModel . properties , leaseId : containerModel . leaseId } ; } ) ; } public async breakContainerLease ( context , account , container , breakPeriod , options = { } ) { return this . sequelize . transaction ( async ( t ) => { const findResult = await ContainersModel . findOne ( { where : { accountName : account , containerName : container } , transaction : t } ) ; validateWriteConditions ( context , options . modifiedAccessConditions , findResult ? this . convertDbModelToContainerModel ( findResult ) : undefined ) ; if ( findResult === null || findResult === undefined ) { throw StorageErrorFactory . getContainerNotFound ( context . contextId ) ; } const containerModel = this . convertDbModelToContainerModel ( findResult ) ; LeaseFactory . createLeaseState ( new ContainerLeaseAdapter ( containerModel ) , context ) . break ( breakPeriod ) . sync ( new ContainerLeaseSyncer ( containerModel ) ) ; await ContainersModel . update ( this . convertLeaseToDbModel ( new ContainerLeaseAdapter ( containerModel ) ) , { where : { accountName : account , containerName : container } , transaction : t } ) ; const leaseTimeSeconds = containerModel . properties . leaseState === Models . LeaseStateType . Breaking && containerModel . leaseBreakTime ? Math . round ( ( containerModel . leaseBreakTime . getTime ( ) - context . startTime ! . getTime ( ) ) / 1000 ) : 0 ; return { properties : containerModel . properties , leaseTime : leaseTimeSeconds } ; } ) ; } public async changeContainerLease ( context , account , container , leaseId , proposedLeaseId , options = { } ) { return this . sequelize . transaction ( async ( t ) => { const findResult = await ContainersModel . findOne ( { where : { accountName : account , containerName : container } , transaction : t } ) ; validateWriteConditions ( context , options . modifiedAccessConditions , findResult ? this . convertDbModelToContainerModel ( findResult ) : undefined ) ; if ( findResult === null || findResult === undefined ) { throw StorageErrorFactory . getContainerNotFound ( context . contextId ) ; } const containerModel = this . convertDbModelToContainerModel ( findResult ) ; LeaseFactory . createLeaseState ( new ContainerLeaseAdapter ( containerModel ) , context ) . change ( leaseId , proposedLeaseId ) . sync ( new ContainerLeaseSyncer ( containerModel ) ) ; await ContainersModel . update ( this . convertLeaseToDbModel ( new ContainerLeaseAdapter ( containerModel ) ) , { where : { accountName : account , containerName : container } , transaction : t } ) ; return { properties : containerModel . properties , leaseId : containerModel . leaseId } ; } ) ; } public async checkContainerExist ( context , account , container ) { await this . assertContainerExists ( context , account , container , undefined ) ; } public async createBlob ( context , blob , leaseAccessConditions ? , modifiedAccessConditions ? ) { return this . sequelize . transaction ( async ( t ) => { await this . assertContainerExists ( context , blob . accountName , blob . containerName , t ) ; const blobFindResult = await BlobsModel . findOne ( { where : { accountName : blob . accountName , containerName : blob . containerName , blobName : blob . name , snapshot : blob . snapshot , deleting : 0 , isCommitted : true } , transaction : t } ) ; validateWriteConditions ( context , modifiedAccessConditions , blobFindResult ? this . convertDbModelToBlobModel ( blobFindResult ) : undefined ) ; if ( modifiedAccessConditions && modifiedAccessConditions . ifNoneMatch === "*" && blobFindResult ) { throw StorageErrorFactory . getBlobAlreadyExists ( context . contextId ) ; } if ( blobFindResult ) { const blobModel = this . convertDbModelToBlobModel ( blobFindResult ) ; LeaseFactory . createLeaseState ( new BlobLeaseAdapter ( blobModel ) , context ) . validate ( new BlobWriteLeaseValidator ( leaseAccessConditions ) ) . sync ( new BlobLeaseSyncer ( blob ) ) ; if ( blobModel . properties !== undefined && blobModel . properties . accessTier === Models . AccessTier . Archive ) { throw StorageErrorFactory . getBlobArchived ( context . contextId ) ; } } await BlobsModel . upsert ( this . convertBlobModelToDbModel ( blob ) , { transaction : t } ) ; } ) ; } public async downloadBlob ( context , account , container , blob , snapshot = "" , leaseAccessConditions ? , modifiedAccessConditions ? ) { return this . sequelize . transaction ( async ( t ) => { await this . assertContainerExists ( context , account , container , t ) ; const blobFindResult = await BlobsModel . findOne ( { where : { accountName : account , containerName : container , blobName : blob , snapshot , deleting : 0 , isCommitted : true } , transaction : t } ) ; validateReadConditions ( context , modifiedAccessConditions , blobFindResult ? this . convertDbModelToBlobModel ( blobFindResult ) : undefined ) ; if ( blobFindResult === null || blobFindResult === undefined ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ) ; } const blobModel = this . convertDbModelToBlobModel ( blobFindResult ) ; return LeaseFactory . createLeaseState ( new BlobLeaseAdapter ( blobModel ) , context ) . validate ( new BlobReadLeaseValidator ( leaseAccessConditions ) ) . sync ( new BlobLeaseSyncer ( blobModel ) ) ; } ) ; } public async listBlobs ( context , account , container , blob ? , prefix = "" , maxResults = DEFAULT_LIST_BLOBS_MAX_RESULTS , marker ? , includeSnapshots ? , includeUncommittedBlobs ? ) { return this . sequelize . transaction ( async ( t ) => { await this . assertContainerExists ( context , account , container , t ) ; const whereQuery = { accountName : account , containerName : container } ; if ( blob !== undefined ) { whereQuery . blobName = blob ; } else { if ( prefix . length > 0 ) { whereQuery . blobName = { [ Op . like ] : `${ prefix }%` } ; } if ( marker !== undefined ) { if ( whereQuery . blobName !== undefined ) { whereQuery . blobName [ Op . gt ] = marker ; } else { whereQuery . blobName = { [ Op . gt ] : marker } ; } } } if ( ! includeSnapshots ) { whereQuery . snapshot = "" ; } if ( ! includeUncommittedBlobs ) { whereQuery . isCommitted = true ; } whereQuery . deleting = 0 ; const blobFindResult = await BlobsModel . findAll ( { limit : maxResults + 1 , where : whereQuery as any , order : [ [ "blobName" , "ASC" ] ] , transaction : t } ) ; const leaseUpdateMapper = ( model ) => { const blobModel = this . convertDbModelToBlobModel ( model ) ; return LeaseFactory . createLeaseState ( new BlobLeaseAdapter ( blobModel ) , context ) . sync ( new BlobLeaseSyncer ( blobModel ) ) ; } ; if ( blobFindResult . length <= maxResults ) { return [ blobFindResult . map ( leaseUpdateMapper ) , undefined ] ; } else { blobFindResult . pop ( ) ; const tail = blobFindResult [ blobFindResult . length - 1 ] ; const nextMarker = this . getModelValue < string > ( tail , "blobName" , true ) ; return [ blobFindResult . map ( leaseUpdateMapper ) , nextMarker ] ; } } ) ; } public async listAllBlobs ( maxResults = DEFAULT_LIST_BLOBS_MAX_RESULTS , marker ? , includeSnapshots ? , includeUncommittedBlobs ? ) { const whereQuery = { } ; if ( marker !== undefined ) { whereQuery . blobName = { [ Op . gt ] : marker } ; } if ( ! includeSnapshots ) { whereQuery . snapshot = "" ; } if ( ! includeUncommittedBlobs ) { whereQuery . isCommitted = true ; } whereQuery . deleting = 0 ; const blobFindResult = await BlobsModel . findAll ( { limit : maxResults + 1 , where : whereQuery as any , order : [ [ "blobName" , "ASC" ] ] } ) ; if ( blobFindResult . length <= maxResults ) { return [ blobFindResult . map ( this . convertDbModelToBlobModel . bind ( this ) ) , undefined ] ; } else { blobFindResult . pop ( ) ; const tail = blobFindResult [ blobFindResult . length - 1 ] ; const nextMarker = this . getModelValue < string > ( tail , "blobName" , true ) ; return [ blobFindResult . map ( this . convertDbModelToBlobModel . bind ( this ) ) , nextMarker ] ; } } public async stageBlock ( context , block , leaseAccessConditions ? ) { await this . sequelize . transaction ( async ( t ) => { await this . assertContainerExists ( context , block . accountName , block . containerName , t ) ; const blobFindResult = await BlobsModel . findOne ( { where : { accountName : block . accountName , containerName : block . containerName , blobName : block . blobName , snapshot : "" , deleting : 0 } , transaction : t } ) ; if ( blobFindResult !== null && blobFindResult !== undefined ) { const blobModel = this . convertDbModelToBlobModel ( blobFindResult ) ; if ( blobModel . isCommitted === true ) { LeaseFactory . createLeaseState ( new BlobLeaseAdapter ( blobModel ) , context ) . validate ( new BlobWriteLeaseValidator ( leaseAccessConditions ) ) ; } const existBlock = await BlocksModel . findOne ( { attributes : [ "blockName" ] , where : { accountName : block . accountName , containerName : block . containerName , blobName : block . blobName , deleting : 0 } , order : [ [ "id" , "ASC" ] ] , transaction : t } ) ; if ( existBlock && Buffer . from ( this . getModelValue < string > ( existBlock , "blockName" , true ) , "base64" ) . length !== Buffer . from ( block . name , "base64" ) . length ) { throw StorageErrorFactory . getInvalidBlobOrBlock ( context . contextId ) ; } } else { const newBlob = { deleted : false , accountName : block . accountName , containerName : block . containerName , name : block . blobName , properties : { creationTime : context . startTime ! , lastModified : context . startTime ! , etag : newEtag ( ) , contentLength : 0 , blobType : Models . BlobType . BlockBlob } , snapshot : "" , isCommitted : false } ; await BlobsModel . upsert ( this . convertBlobModelToDbModel ( newBlob ) , { transaction : t } ) ; } await BlocksModel . upsert ( { accountName : block . accountName , containerName : block . containerName , blobName : block . blobName , blockName : block . name , size : block . size , persistency : this . serializeModelValue ( block . persistency ) } , { transaction : t } ) ; } ) ; } public getBlockList ( context , account , container , blob , snapshot = "" , isCommitted ? , leaseAccessConditions ? ) { return this . sequelize . transaction ( async ( t ) => { await this . assertContainerExists ( context , account , container , t ) ; const blobFindResult = await BlobsModel . findOne ( { where : { accountName : account , containerName : container , blobName : blob , snapshot , deleting : 0 } , transaction : t } ) ; if ( blobFindResult === null || blobFindResult === undefined ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ) ; } const blobModel = this . convertDbModelToBlobModel ( blobFindResult ) ; LeaseFactory . createLeaseState ( new BlobLeaseAdapter ( blobModel ) , context ) . validate ( new BlobReadLeaseValidator ( leaseAccessConditions ) ) ; const res = { uncommittedBlocks : [ ] , committedBlocks : [ ] } ; if ( isCommitted !== false ) { res . committedBlocks = blobModel . committedBlocksInOrder || [ ] ; } if ( isCommitted !== true ) { const blocks = await BlocksModel . findAll ( { attributes : [ "blockName" , "size" ] , where : { accountName : account , containerName : container , blobName : blob , deleting : 0 } , order : [ [ "id" , "ASC" ] ] , transaction : t } ) ; for ( const item of blocks ) { const block = { name : this . getModelValue < string > ( item , "blockName" , true ) , size : this . getModelValue < number > ( item , "size" , true ) } ; res . uncommittedBlocks . push ( block ) ; } } return res ; } ) ; } public async commitBlockList ( context , blob , blockList , leaseAccessConditions ? , modifiedAccessConditions ? ) { await this . sequelize . transaction ( async ( t ) => { await this . assertContainerExists ( context , blob . accountName , blob . containerName , t ) ; const pCommittedBlocksMap = new Map ( ) ; const pUncommittedBlocksMap = new Map ( ) ; const badRequestError = StorageErrorFactory . getInvalidBlockList ( context . contextId ) ; const blobFindResult = await BlobsModel . findOne ( { where : { accountName : blob . accountName , containerName : blob . containerName , blobName : blob . name , snapshot : blob . snapshot , deleting : 0 , isCommitted : true } , transaction : t } ) ; validateWriteConditions ( context , modifiedAccessConditions , blobFindResult ? this . convertDbModelToBlobModel ( blobFindResult ) : undefined ) ; let creationTime = blob . properties . creationTime || context . startTime ; if ( blobFindResult !== null && blobFindResult !== undefined ) { const blobModel = this . convertDbModelToBlobModel ( blobFindResult ) ; if ( modifiedAccessConditions && modifiedAccessConditions . ifNoneMatch === "*" && blobModel && blobModel . isCommitted ) { throw StorageErrorFactory . getBlobAlreadyExists ( context . contextId ) ; } creationTime = blobModel . properties . creationTime || creationTime ; LeaseFactory . createLeaseState ( new BlobLeaseAdapter ( blobModel ) , context ) . validate ( new BlobWriteLeaseValidator ( leaseAccessConditions ) ) ; const committedBlocksInOrder = blobModel . committedBlocksInOrder ; for ( const pBlock of committedBlocksInOrder || [ ] ) { pCommittedBlocksMap . set ( pBlock . name , pBlock ) ; } } const blockFindResult = await BlocksModel . findAll ( { where : { accountName : blob . accountName , containerName : blob . containerName , blobName : blob . name , deleting : 0 } , transaction : t } ) ; for ( const item of blockFindResult ) { const block = { name : this . getModelValue < string > ( item , "blockName" , true ) , size : this . getModelValue < number > ( item , "size" , true ) , persistency : this . deserializeModelValue ( item , "persistency" ) } ; pUncommittedBlocksMap . set ( block . name , block ) ; } const selectedBlockList = [ ] ; for ( const block of blockList ) { switch ( block . blockCommitType . toLowerCase ( ) ) { case "uncommitted" : const pUncommittedBlock = pUncommittedBlocksMap . get ( block . blockName ) ; if ( pUncommittedBlock === undefined ) { throw badRequestError ; } else { selectedBlockList . push ( pUncommittedBlock ) ; } break ; case "committed" : const pCommittedBlock = pCommittedBlocksMap . get ( block . blockName ) ; if ( pCommittedBlock === undefined ) { throw badRequestError ; } else { selectedBlockList . push ( pCommittedBlock ) ; } break ; case "latest" : const pLatestBlock = pUncommittedBlocksMap . get ( block . blockName ) || pCommittedBlocksMap . get ( block . blockName ) ; if ( pLatestBlock === undefined ) { throw badRequestError ; } else { selectedBlockList . push ( pLatestBlock ) ; } break ; default : throw badRequestError ; } } const commitBlockBlob = { ... blob , deleted : false , committedBlocksInOrder : selectedBlockList , properties : { ... blob . properties , creationTime , lastModified : blob . properties . lastModified || context . startTime , contentLength : selectedBlockList . map ( ( block ) => block . size ) . reduce ( ( total , val ) => { return total + val ; } , 0 ) , blobType : Models . BlobType . BlockBlob } } ; new BlobLeaseSyncer ( commitBlockBlob ) . sync ( { leaseId : undefined , leaseExpireTime : undefined , leaseDurationSeconds : undefined , leaseBreakTime : undefined , leaseDurationType : undefined , leaseState : undefined , leaseStatus : undefined } ) ; await BlobsModel . upsert ( this . convertBlobModelToDbModel ( commitBlockBlob ) , { transaction : t } ) ; await BlocksModel . update ( { deleting : literal ( "deleting + 1" ) } , { where : { accountName : blob . accountName , containerName : blob . containerName , blobName : blob . name } , transaction : t } ) ; } ) ; } public async getBlobProperties ( context , account , container , blob , snapshot = "" , leaseAccessConditions ? , modifiedAccessConditions ? ) { return this . sequelize . transaction ( async ( t ) => { await this . assertContainerExists ( context , account , container , t ) ; const blobFindResult = await BlobsModel . findOne ( { where : { accountName : account , containerName : container , blobName : blob , snapshot , deleting : 0 , isCommitted : true } , transaction : t } ) ; validateReadConditions ( context , modifiedAccessConditions , blobFindResult ? this . convertDbModelToBlobModel ( blobFindResult ) : undefined ) ; if ( blobFindResult === null || blobFindResult === undefined ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ) ; } const blobModel = this . convertDbModelToBlobModel ( blobFindResult ) ; if ( ! blobModel . isCommitted ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ) ; } return LeaseFactory . createLeaseState ( new BlobLeaseAdapter ( blobModel ) , context ) . validate ( new BlobReadLeaseValidator ( leaseAccessConditions ) ) . sync ( new BlobLeaseSyncer ( blobModel ) ) ; } ) ; } public undeleteBlob ( ) { throw new Error ( "Method not implemented." ) ; } public async createSnapshot ( context , account , container , blob , leaseAccessConditions ? , metadata ? , modifiedAccessConditions ? ) { return this . sequelize . transaction ( async ( t ) => { await this . assertContainerExists ( context , account , container , t ) ; const blobFindResult = await BlobsModel . findOne ( { where : { accountName : account , containerName : container , blobName : blob , snapshot : "" , deleting : 0 , isCommitted : true } , transaction : t } ) ; validateReadConditions ( context , modifiedAccessConditions , blobFindResult ? this . convertDbModelToBlobModel ( blobFindResult ) : undefined ) ; if ( blobFindResult === null || blobFindResult === undefined ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ) ; } const snapshotBlob = this . convertDbModelToBlobModel ( blobFindResult ) ; LeaseFactory . createLeaseState ( new BlobLeaseAdapter ( snapshotBlob ) , context ) . validate ( new BlobReadLeaseValidator ( leaseAccessConditions ) ) ; const snapshotTime = convertDateTimeStringMsTo7Digital ( context . startTime ! . toISOString ( ) ) ; snapshotBlob . snapshot = snapshotTime ; snapshotBlob . metadata = metadata || snapshotBlob . metadata ; new BlobLeaseSyncer ( snapshotBlob ) . sync ( { leaseId : undefined , leaseExpireTime : undefined , leaseDurationSeconds : undefined , leaseBreakTime : undefined , leaseDurationType : undefined , leaseState : undefined , leaseStatus : undefined } ) ; await BlobsModel . upsert ( this . convertBlobModelToDbModel ( snapshotBlob ) , { transaction : t } ) ; return { properties : snapshotBlob . properties , snapshot : snapshotTime } ; } ) ; } public async deleteBlob ( context , account , container , blob , options ) { await this . sequelize . transaction ( async ( t ) => { await this . assertContainerExists ( context , account , container , t ) ; const blobFindResult = await BlobsModel . findOne ( { where : { accountName : account , containerName : container , blobName : blob , snapshot : options . snapshot === undefined ? "" : options . snapshot , deleting : 0 , isCommitted : true } , transaction : t } ) ; validateWriteConditions ( context , options . modifiedAccessConditions , blobFindResult ? this . convertDbModelToBlobModel ( blobFindResult ) : undefined ) ; if ( blobFindResult === null || blobFindResult === undefined ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ) ; } const blobModel = this . convertDbModelToBlobModel ( blobFindResult ) ; const againstBaseBlob = blobModel . snapshot === "" ; if ( againstBaseBlob ) { LeaseFactory . createLeaseState ( new BlobLeaseAdapter ( blobModel ) , context ) . validate ( new BlobWriteLeaseValidator ( options . leaseAccessConditions ) ) ; } if ( ! againstBaseBlob && options . deleteSnapshots !== undefined ) { throw StorageErrorFactory . getInvalidOperation ( context . contextId , "Invalid operation against a blob snapshot." ) ; } if ( againstBaseBlob && options . deleteSnapshots === undefined ) { const count = await BlobsModel . count ( { where : { accountName : account , containerName : container , blobName : blob , deleting : 0 } , transaction : t } ) ; if ( count > 1 ) { throw StorageErrorFactory . getSnapshotsPresent ( context . contextId ! ) ; } else { await BlobsModel . update ( { deleting : literal ( "deleting + 1" ) } , { where : { accountName : account , containerName : container , blobName : blob } , transaction : t } ) ; await BlocksModel . update ( { deleting : literal ( "deleting + 1" ) } , { where : { accountName : account , containerName : container , blobName : blob } , transaction : t } ) ; } } if ( ! againstBaseBlob ) { await BlobsModel . update ( { deleting : literal ( "deleting + 1" ) } , { where : { accountName : account , containerName : container , blobName : blob , snapshot : blobModel . snapshot } , transaction : t } ) ; } if ( againstBaseBlob && options . deleteSnapshots === Models . DeleteSnapshotsOptionType . Include ) { await BlobsModel . update ( { deleting : literal ( "deleting + 1" ) } , { where : { accountName : account , containerName : container , blobName : blob } , transaction : t } ) ; await BlocksModel . update ( { deleting : literal ( "deleting + 1" ) } , { where : { accountName : account , containerName : container , blobName : blob } , transaction : t } ) ; } if ( againstBaseBlob && options . deleteSnapshots === Models . DeleteSnapshotsOptionType . Only ) { await BlobsModel . update ( { deleting : literal ( "deleting + 1" ) } , { where : { accountName : account , containerName : container , blobName : blob , snapshot : { [ Op . gt ] : "" } } , transaction : t } ) ; } } ) ; } public async setBlobHTTPHeaders ( context , account , container , blob , leaseAccessConditions , blobHTTPHeaders , modifiedAccessConditions ? ) { return this . sequelize . transaction ( async ( t ) => { await this . assertContainerExists ( context , account , container , t ) ; const blobFindResult = await BlobsModel . findOne ( { where : { accountName : account , containerName : container , blobName : blob , snapshot : "" , deleting : 0 , isCommitted : true } , transaction : t } ) ; validateWriteConditions ( context , modifiedAccessConditions , blobFindResult ? this . convertDbModelToBlobModel ( blobFindResult ) : undefined ) ; if ( blobFindResult === null || blobFindResult === undefined ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ) ; } const blobModel = this . convertDbModelToBlobModel ( blobFindResult ) ; LeaseFactory . createLeaseState ( new BlobLeaseAdapter ( blobModel ) , context ) . validate ( new BlobWriteLeaseValidator ( leaseAccessConditions ) ) . sync ( new BlobWriteLeaseSyncer ( blobModel ) ) ; if ( blobHTTPHeaders !== undefined ) { blobModel . properties . cacheControl = blobHTTPHeaders . blobCacheControl ; blobModel . properties . contentType = blobHTTPHeaders . blobContentType ; blobModel . properties . contentMD5 = blobHTTPHeaders . blobContentMD5 ; blobModel . properties . contentEncoding = blobHTTPHeaders . blobContentEncoding ; blobModel . properties . contentLanguage = blobHTTPHeaders . blobContentLanguage ; blobModel . properties . contentDisposition = blobHTTPHeaders . blobContentDisposition ; } blobModel . properties . etag = newEtag ( ) ; blobModel . properties . lastModified = context . startTime ? context . startTime : new Date ( ) ; await BlobsModel . update ( this . convertBlobModelToDbModel ( blobModel ) , { where : { accountName : account , containerName : container , blobName : blob , snapshot : "" , deleting : 0 } , transaction : t } ) ; return blobModel . properties ; } ) ; } public setBlobMetadata ( context , account , container , blob , leaseAccessConditions , metadata , modifiedAccessConditions ? ) { return this . sequelize . transaction ( async ( t ) => { await this . assertContainerExists ( context , account , container , t ) ; const blobFindResult = await BlobsModel . findOne ( { where : { accountName : account , containerName : container , blobName : blob , snapshot : "" , deleting : 0 , isCommitted : true } , transaction : t } ) ; validateWriteConditions ( context , modifiedAccessConditions , blobFindResult ? this . convertDbModelToBlobModel ( blobFindResult ) : undefined ) ; if ( blobFindResult === null || blobFindResult === undefined ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ) ; } const blobModel = this . convertDbModelToBlobModel ( blobFindResult ) ; LeaseFactory . createLeaseState ( new BlobLeaseAdapter ( blobModel ) , context ) . validate ( new BlobWriteLeaseValidator ( leaseAccessConditions ) ) . sync ( new BlobWriteLeaseSyncer ( blobModel ) ) ; const lastModified = context . startTime ! || new Date ( ) ; const etag = newEtag ( ) ; await BlobsModel . update ( { metadata : this . serializeModelValue ( metadata ) || null , lastModified , etag , ... this . convertLeaseToDbModel ( new BlobLeaseAdapter ( blobModel ) ) } , { where : { accountName : account , containerName : container , blobName : blob , snapshot : "" , deleting : 0 } , transaction : t } ) ; const ret = { lastModified , etag , leaseStatus : blobModel . properties . leaseStatus , leaseDuration : blobModel . properties . leaseDuration , leaseState : blobModel . properties . leaseState } ; return ret ; } ) ; } public async acquireBlobLease ( context , account , container , blob , duration , proposedLeaseId ? , options = { } ) { return this . sequelize . transaction ( async ( t ) => { await this . assertContainerExists ( context , account , container , t ) ; const blobFindResult = await BlobsModel . findOne ( { where : { accountName : account , containerName : container , blobName : blob , snapshot : "" , deleting : 0 , isCommitted : true } , transaction : t } ) ; validateWriteConditions ( context , options . modifiedAccessConditions , blobFindResult ? this . convertDbModelToBlobModel ( blobFindResult ) : undefined ) ; if ( blobFindResult === null || blobFindResult === undefined ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ) ; } const blobModel = this . convertDbModelToBlobModel ( blobFindResult ) ; const lease = LeaseFactory . createLeaseState ( new BlobLeaseAdapter ( blobModel ) , context ) . acquire ( duration , proposedLeaseId ) . lease ; await BlobsModel . update ( this . convertLeaseToDbModel ( lease ) , { where : { accountName : account , containerName : container , blobName : blob , snapshot : "" , deleting : 0 } , transaction : t } ) ; return { properties : blobModel . properties , leaseId : lease . leaseId } ; } ) ; } public async releaseBlobLease ( context , account , container , blob , leaseId , options = { } ) { return this . sequelize . transaction ( async ( t ) => { await this . assertContainerExists ( context , account , container , t ) ; const blobFindResult = await BlobsModel . findOne ( { where : { accountName : account , containerName : container , blobName : blob , snapshot : "" , deleting : 0 , isCommitted : true } , transaction : t } ) ; validateWriteConditions ( context , options . modifiedAccessConditions , blobFindResult ? this . convertDbModelToBlobModel ( blobFindResult ) : undefined ) ; if ( blobFindResult === null || blobFindResult === undefined ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ) ; } const blobModel = this . convertDbModelToBlobModel ( blobFindResult ) ; const lease = LeaseFactory . createLeaseState ( new BlobLeaseAdapter ( blobModel ) , context ) . release ( leaseId ) . lease ; await BlobsModel . update ( this . convertLeaseToDbModel ( lease ) , { where : { accountName : account , containerName : container , blobName : blob , snapshot : "" , deleting : 0 } , transaction : t } ) ; return blobModel . properties ; } ) ; } public async renewBlobLease ( context , account , container , blob , leaseId , options = { } ) { return this . sequelize . transaction ( async ( t ) => { await this . assertContainerExists ( context , account , container , t ) ; const blobFindResult = await BlobsModel . findOne ( { where : { accountName : account , containerName : container , blobName : blob , snapshot : "" , deleting : 0 , isCommitted : true } , transaction : t } ) ; validateWriteConditions ( context , options . modifiedAccessConditions , blobFindResult ? this . convertDbModelToBlobModel ( blobFindResult ) : undefined ) ; if ( blobFindResult === null || blobFindResult === undefined ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ) ; } const blobModel = this . convertDbModelToBlobModel ( blobFindResult ) ; const lease = LeaseFactory . createLeaseState ( new BlobLeaseAdapter ( blobModel ) , context ) . renew ( leaseId ) . lease ; await BlobsModel . update ( this . convertLeaseToDbModel ( lease ) , { where : { accountName : account , containerName : container , blobName : blob , snapshot : "" , deleting : 0 } , transaction : t } ) ; return { properties : blobModel . properties , leaseId : lease . leaseId } ; } ) ; } public async changeBlobLease ( context , account , container , blob , leaseId , proposedLeaseId , options = { } ) { return this . sequelize . transaction ( async ( t ) => { await this . assertContainerExists ( context , account , container , t ) ; const blobFindResult = await BlobsModel . findOne ( { where : { accountName : account , containerName : container , blobName : blob , snapshot : "" , deleting : 0 , isCommitted : true } , transaction : t } ) ; validateWriteConditions ( context , options . modifiedAccessConditions , blobFindResult ? this . convertDbModelToBlobModel ( blobFindResult ) : undefined ) ; if ( blobFindResult === null || blobFindResult === undefined ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ) ; } const blobModel = this . convertDbModelToBlobModel ( blobFindResult ) ; const lease = LeaseFactory . createLeaseState ( new BlobLeaseAdapter ( blobModel ) , context ) . change ( leaseId , proposedLeaseId ) . lease ; await BlobsModel . update ( this . convertLeaseToDbModel ( lease ) , { where : { accountName : account , containerName : container , blobName : blob , snapshot : "" , deleting : 0 } , transaction : t } ) ; return { properties : blobModel . properties , leaseId : lease . leaseId } ; } ) ; } public async breakBlobLease ( context , account , container , blob , breakPeriod , options = { } ) { return this . sequelize . transaction ( async ( t ) => { await this . assertContainerExists ( context , account , container , t ) ; const blobFindResult = await BlobsModel . findOne ( { where : { accountName : account , containerName : container , blobName : blob , snapshot : "" , deleting : 0 , isCommitted : true } , transaction : t } ) ; validateWriteConditions ( context , options . modifiedAccessConditions , blobFindResult ? this . convertDbModelToBlobModel ( blobFindResult ) : undefined ) ; if ( blobFindResult === null || blobFindResult === undefined ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ) ; } const blobModel = this . convertDbModelToBlobModel ( blobFindResult ) ; const lease = LeaseFactory . createLeaseState ( new BlobLeaseAdapter ( blobModel ) , context ) . break ( breakPeriod ) . lease ; const leaseTimeSeconds = lease . leaseState === Models . LeaseStateType . Breaking && lease . leaseBreakTime ? Math . round ( ( lease . leaseBreakTime . getTime ( ) - context . startTime ! . getTime ( ) ) / 1000 ) : 0 ; await BlobsModel . update ( this . convertLeaseToDbModel ( lease ) , { where : { accountName : account , containerName : container , blobName : blob , snapshot : "" , deleting : 0 } , transaction : t } ) ; return { properties : blobModel . properties , leaseTime : leaseTimeSeconds } ; } ) ; } public async checkBlobExist ( context , account , container , blob , snapshot ? ) { await this . sequelize . transaction ( async ( t ) => { await this . assertContainerExists ( context , account , container , t ) ; const res = await BlobsModel . findOne ( { where : { accountName : account , containerName : container , blobName : blob , snapshot : snapshot ? snapshot : "" , deleting : 0 } , transaction : t } ) ; if ( res === null || res === undefined ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ) ; } } ) ; } public async getBlobType ( account , container , blob , snapshot ? ) { const res = await BlobsModel . findOne ( { where : { accountName : account , containerName : container , blobName : blob , snapshot : snapshot ? snapshot : "" , deleting : 0 } } ) ; if ( res === null || res === undefined ) { return undefined ; } const blobType = this . getModelValue < Models . BlobType > ( res , "blobType" , true ) ; const isCommitted = this . getModelValue < boolean > ( res , "isCommitted" , true ) ; return { blobType , isCommitted } ; } public startCopyFromURL ( context , source , destination , copySource , metadata , tier , options = { } ) { return this . sequelize . transaction ( async ( t ) => { const sourceBlob = await this . getBlobWithLeaseUpdated ( source . account , source . container , source . blob , source . snapshot , context , true , true , t ) ; options . sourceModifiedAccessConditions = options . sourceModifiedAccessConditions || { } ; validateReadConditions ( context , { ifModifiedSince : options . sourceModifiedAccessConditions . sourceIfModifiedSince , ifUnmodifiedSince : options . sourceModifiedAccessConditions . sourceIfUnmodifiedSince , ifMatch : options . sourceModifiedAccessConditions . sourceIfMatches , ifNoneMatch : options . sourceModifiedAccessConditions . sourceIfNoneMatch } , sourceBlob ) ; const destBlob = await this . getBlobWithLeaseUpdated ( destination . account , destination . container , destination . blob , undefined , context , false , undefined , t ) ; validateWriteConditions ( context , options . modifiedAccessConditions , destBlob ) ; if ( destBlob ) { new BlobWriteLeaseValidator ( options . leaseAccessConditions ) . validate ( new BlobLeaseAdapter ( destBlob ) , context ) ; } if ( sourceBlob === undefined || sourceBlob . deleted || ! sourceBlob . isCommitted ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ! ) ; } if ( sourceBlob . properties . accessTier === Models . AccessTier . Archive && ( tier === undefined || source . account !== destination . account ) ) { throw StorageErrorFactory . getBlobArchived ( context . contextId ! ) ; } await this . assertContainerExists ( context , destination . account , destination . container , t ) ; const copiedBlob = { name : destination . blob , deleted : false , snapshot : "" , properties : { ... sourceBlob . properties , creationTime : context . startTime ! , lastModified : context . startTime ! , etag : newEtag ( ) , leaseStatus : destBlob !== undefined ? destBlob . properties . leaseStatus : Models . LeaseStatusType . Unlocked , leaseState : destBlob !== undefined ? destBlob . properties . leaseState : Models . LeaseStateType . Available , leaseDuration : destBlob !== undefined ? destBlob . properties . leaseDuration : undefined , copyId : uuid ( ) , copyStatus : Models . CopyStatusType . Success , copySource , copyProgress : sourceBlob . properties . contentLength ? `${ sourceBlob . properties . contentLength }/${ sourceBlob . properties . contentLength }` : undefined , copyCompletionTime : context . startTime , copyStatusDescription : undefined , incrementalCopy : false , destinationSnapshot : undefined , deletedTime : undefined , remainingRetentionDays : undefined , archiveStatus : undefined , accessTierChangeTime : undefined } , metadata : metadata === undefined || Object . keys ( metadata ) . length === 0 ? { ... sourceBlob . metadata } : metadata , accountName : destination . account , containerName : destination . container , pageRangesInOrder : sourceBlob . pageRangesInOrder , isCommitted : sourceBlob . isCommitted , leaseDurationSeconds : destBlob !== undefined ? destBlob . leaseDurationSeconds : undefined , leaseId : destBlob !== undefined ? destBlob . leaseId : undefined , leaseExpireTime : destBlob !== undefined ? destBlob . leaseExpireTime : undefined , leaseBreakTime : destBlob !== undefined ? destBlob . leaseBreakTime : undefined , committedBlocksInOrder : sourceBlob . committedBlocksInOrder , persistency : sourceBlob . persistency } ; if ( copiedBlob . properties . blobType === Models . BlobType . BlockBlob && tier !== undefined ) { copiedBlob . properties . accessTier = this . parseTier ( tier ) ; if ( copiedBlob . properties . accessTier === undefined ) { throw StorageErrorFactory . getInvalidHeaderValue ( context . contextId , { HeaderName : "x-ms-access-tier" , HeaderValue : `${ tier }` } ) ; } } if ( copiedBlob . properties . blobType === Models . BlobType . PageBlob && tier !== undefined ) { throw StorageErrorFactory . getInvalidHeaderValue ( context . contextId , { HeaderName : "x-ms-access-tier" , HeaderValue : `${ tier }` } ) ; } await BlobsModel . upsert ( this . convertBlobModelToDbModel ( copiedBlob ) , { transaction : t } ) ; return copiedBlob . properties ; } ) ; } public copyFromURL ( context , source , destination , copySource , metadata ) { throw new Error ( "Method not implemented." ) ; } public setTier ( context , account , container , blob , tier , leaseAccessConditions ? ) { return this . sequelize . transaction ( async ( t ) => { await this . assertContainerExists ( context , account , container , t ) ; const blobFindResult = await BlobsModel . findOne ( { where : { accountName : account , containerName : container , blobName : blob , snapshot : "" , deleting : 0 , isCommitted : true } , transaction : t } ) ; if ( blobFindResult === null || blobFindResult === undefined ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ) ; } let responseCode = 200 ; const blobModel = this . convertDbModelToBlobModel ( blobFindResult ) ; LeaseFactory . createLeaseState ( new BlobLeaseAdapter ( blobModel ) , context ) . validate ( new BlobWriteLeaseValidator ( leaseAccessConditions ) ) ; const snapshot = blobModel . snapshot ; if ( snapshot !== "" ) { throw StorageErrorFactory . getBlobSnapshotsPresent ( context . contextId ) ; } let accessTier = blobModel . properties . accessTier ; const blobType = blobModel . properties . blobType ; if ( ( tier === Models . AccessTier . Archive || tier === Models . AccessTier . Cool || tier === Models . AccessTier . Hot ) && blobType === Models . BlobType . BlockBlob ) { if ( accessTier === Models . AccessTier . Archive && ( tier === Models . AccessTier . Cool || tier === Models . AccessTier . Hot ) ) { responseCode = 202 ; } accessTier = tier ; } else { throw StorageErrorFactory . getAccessTierNotSupportedForBlobType ( context . contextId ! ) ; } await BlobsModel . update ( { accessTier , accessTierInferred : false , accessTierChangeTime : context . startTime } , { where : { accountName : account , containerName : container , blobName : blob , snapshot : "" , deleting : 0 } , transaction : t } ) ; return responseCode ; } ) ; } public uploadPages ( context , blob , start , end , persistency , leaseAccessConditions ? , modifiedAccessConditions ? ) { throw new Error ( "Method not implemented." ) ; } public clearRange ( context , blob , start , end , leaseAccessConditions ? , modifiedAccessConditions ? ) { throw new Error ( "Method not implemented." ) ; } public getPageRanges ( context , account , container , blob , snapshot ? , leaseAccessConditions ? , modifiedAccessConditions ? ) { throw new Error ( "Method not implemented." ) ; } public resizePageBlob ( context , account , container , blob , blobContentLength , leaseAccessConditions ? , modifiedAccessConditions ? ) { throw new Error ( "Method not implemented." ) ; } public updateSequenceNumber ( context , account , container , blob , sequenceNumberAction , blobSequenceNumber ) { throw new Error ( "Method not implemented." ) ; } public appendBlock ( context , block , leaseAccessConditions ? , modifiedAccessConditions ? , appendPositionAccessConditions ? ) { throw new Error ( "Method not implemented." ) ; } public async listUncommittedBlockPersistencyChunks ( marker = "-1" , maxResults = 2000 ) { return BlocksModel . findAll ( { attributes : [ "id" , "persistency" ] , where : { id : { [ Op . gt ] : parseInt ( marker , 10 ) } , deleting : 0 } , limit : maxResults + 1 , order : [ [ "id" , "ASC" ] ] } ) . then ( ( res ) => { if ( res . length < maxResults ) { return [ res . map ( ( obj ) => { return this . deserializeModelValue ( obj , "persistency" , true ) ; } ) , undefined ] ; } else { res . pop ( ) ; const nextMarker = this . getModelValue < string > ( res [ res . length - 1 ] , "id" , true ) ; return [ res . map ( ( obj ) => this . deserializeModelValue ( obj , "persistency" , true ) ) , nextMarker ] ; } } ) ; } public iteratorExtents ( ) { return new BlobReferredExtentsAsyncIterator ( this ) ; } private async assertContainerExists ( context , account , container , transaction ? , fullResult = false ) { const findResult = await ContainersModel . findOne ( { attributes : fullResult ? undefined : [ "accountName" ] , where : { accountName : account , containerName : container } , transaction } ) ; if ( findResult === undefined || findResult === null ) { throw StorageErrorFactory . getContainerNotFound ( context . contextId ) ; } return findResult ; } private getModelValue < T > ( model , key ) ; private getModelValue < T > ( model , key , isRequired ) ; private getModelValue < T > ( model , key , isRequired ? ) { let value = model . get ( key ) as T | undefined ; if ( value === null ) { value = undefined ; } if ( value === undefined && isRequired === true ) { throw new Error ( `SqlBlobMetadataStore:getModelValue() error. ${ key } is required but value from database model is undefined.` ) ; } return value ; } private deserializeModelValue ( model , key , isRequired = false ) { const rawValue = this . getModelValue < string > ( model , key ) ; if ( typeof rawValue === "string" ) { return JSON . parse ( rawValue ) ; } if ( isRequired ) { throw new Error ( `SqlBlobMetadataStore:deserializeModelValue() error. ${ key } is required but value from database model is undefined.` ) ; } return undefined ; } private serializeModelValue ( value ) { if ( value === undefined || value === null ) { return undefined ; } return JSON . stringify ( value ) ; } private param { * } obj returns { ( Uint8Array | undefined ) } memberof private restoreUint8Array ( obj ) { if ( typeof obj !== "object" ) { return undefined ; } if ( obj instanceof Uint8Array ) { return obj ; } if ( obj . type === "Buffer" ) { obj = obj . data ; } const length = Object . keys ( obj ) . length ; const arr = Buffer . allocUnsafe ( length ) ; for ( let i = 0 ; i < length ; i ++ ) { if ( ! obj . hasOwnProperty ( i ) ) { throw new TypeError ( `Cannot restore sql DB persisted object to Uint8Array. Key ${ i } is missing.` ) ; } arr [ i ] = obj [ i ] ; } return arr ; } private convertDbModelToContainerModel ( dbModel ) { const accountName = this . getModelValue < string > ( dbModel , "accountName" , true ) ; const name = this . getModelValue < string > ( dbModel , "containerName" , true ) ; const containerAcl = this . deserializeModelValue ( dbModel , "containerAcl" ) ; const metadata = this . deserializeModelValue ( dbModel , "metadata" ) ; const lastModified = this . getModelValue < Date > ( dbModel , "lastModified" , true ) ; const etag = this . getModelValue < string > ( dbModel , "etag" , true ) ; const publicAccess = this . deserializeModelValue ( dbModel , "publicAccess" ) ; const lease = this . convertDbModelToLease ( dbModel ) ; const leaseBreakTime = lease . leaseBreakTime ; const leaseExpireTime = lease . leaseExpireTime ; const leaseId = lease . leaseId ; const leaseDurationSeconds = lease . leaseDurationSeconds ; const leaseStatus = lease . leaseStatus ; const leaseState = lease . leaseState ; const leaseDuration = lease . leaseDurationType ; const hasImmutabilityPolicy = this . getModelValue < boolean > ( dbModel , "hasImmutabilityPolicy" ) ; const hasLegalHold = this . getModelValue < boolean > ( dbModel , "hasLegalHold" ) ; const ret = { accountName , name , properties : { lastModified , etag , leaseStatus , leaseDuration , leaseState } , leaseId , leaseBreakTime , leaseExpireTime , leaseDurationSeconds } ; if ( metadata !== undefined ) { ret . metadata = metadata ; } if ( containerAcl !== undefined ) { ret . containerAcl = containerAcl ; } if ( publicAccess !== undefined ) { ret . properties . publicAccess = publicAccess ; } if ( hasImmutabilityPolicy !== undefined ) { ret . properties . hasImmutabilityPolicy = hasImmutabilityPolicy ; } if ( hasLegalHold !== undefined ) { ret . properties . hasLegalHold = hasLegalHold ; } return ret ; } private convertContainerModelToDbModel ( container ) { const lease = new ContainerLeaseAdapter ( container ) . toString ( ) ; return { accountName : container . accountName , containerName : container . name , lastModified : container . properties . lastModified , etag : container . properties . etag , metadata : this . serializeModelValue ( container . metadata ) , containerAcl : this . serializeModelValue ( container . containerAcl ) , publicAccess : this . serializeModelValue ( container . properties . publicAccess ) , lease , hasImmutabilityPolicy : container . properties . hasImmutabilityPolicy , hasLegalHold : container . properties . hasLegalHold } ; } private convertDbModelToBlobModel ( dbModel ) { const contentProperties = this . convertDbModelToBlobContentProperties ( dbModel ) ; const lease = this . convertDbModelToLease ( dbModel ) ; return { accountName : this . getModelValue < string > ( dbModel , "accountName" , true ) , containerName : this . getModelValue < string > ( dbModel , "containerName" , true ) , name : this . getModelValue < string > ( dbModel , "blobName" , true ) , snapshot : this . getModelValue < string > ( dbModel , "snapshot" , true ) , isCommitted : this . getModelValue < boolean > ( dbModel , "isCommitted" , true ) , properties : { lastModified : this . getModelValue < Date > ( dbModel , "lastModified" , true ) , etag : this . getModelValue < string > ( dbModel , "etag" , true ) , leaseDuration : lease . leaseDurationType , creationTime : this . getModelValue < Date > ( dbModel , "creationTime" ) , leaseState : lease . leaseState , leaseStatus : lease . leaseStatus , accessTier : this . getModelValue < Models . AccessTier > ( dbModel , "accessTier" ) , accessTierInferred : this . getModelValue < boolean > ( dbModel , "accessTierInferred" ) , accessTierChangeTime : this . getModelValue < Date > ( dbModel , "accessTierChangeTime" ) , blobSequenceNumber : this . getModelValue < number > ( dbModel , "blobSequenceNumber" ) , blobType : this . getModelValue < Models . BlobType > ( dbModel , "blobType" ) , contentMD5 : contentProperties ? this . restoreUint8Array ( contentProperties . contentMD5 ) : undefined , contentDisposition : contentProperties ? contentProperties . contentDisposition : undefined , contentEncoding : contentProperties ? contentProperties . contentEncoding : undefined , contentLanguage : contentProperties ? contentProperties . contentLanguage : undefined , contentLength : contentProperties ? contentProperties . contentLength : undefined , contentType : contentProperties ? contentProperties . contentType : undefined , cacheControl : contentProperties ? contentProperties . cacheControl : undefined } , leaseDurationSeconds : lease . leaseDurationSeconds , leaseBreakTime : lease . leaseBreakTime , leaseExpireTime : lease . leaseExpireTime , leaseId : lease . leaseId , persistency : this . deserializeModelValue ( dbModel , "persistency" ) , committedBlocksInOrder : this . deserializeModelValue ( dbModel , "committedBlocksInOrder" ) , metadata : this . deserializeModelValue ( dbModel , "metadata" ) } ; } private convertBlobModelToDbModel ( blob ) { const contentProperties = this . convertBlobContentPropertiesToDbModel ( blob . properties ) ; const lease = this . convertLeaseToDbModel ( new BlobLeaseAdapter ( blob ) ) ; return { accountName : blob . accountName , containerName : blob . containerName , blobName : blob . name , snapshot : blob . snapshot , blobType : blob . properties . blobType , blobSequenceNumber : blob . properties . blobSequenceNumber || null , isCommitted : blob . isCommitted , lastModified : blob . properties . lastModified , creationTime : blob . properties . creationTime || null , etag : blob . properties . etag , accessTier : blob . properties . accessTier || null , accessTierChangeTime : blob . properties . accessTierChangeTime || null , accessTierInferred : blob . properties . accessTierInferred || null , leaseBreakExpireTime : blob . leaseBreakTime || null , leaseExpireTime : blob . leaseExpireTime || null , leaseId : blob . leaseId || null , leasedurationNumber : blob . leaseDurationSeconds || null , leaseDuration : blob . properties . leaseDuration || null , leaseStatus : blob . properties . leaseStatus || null , leaseState : blob . properties . leaseState || null , ... lease , persistency : this . serializeModelValue ( blob . persistency ) || null , committedBlocksInOrder : this . serializeModelValue ( blob . committedBlocksInOrder ) || null , metadata : this . serializeModelValue ( blob . metadata ) || null , ... contentProperties } ; } private convertDbModelToBlobContentProperties ( dbModel ) { return this . deserializeModelValue ( dbModel , "contentProperties" ) ; } private convertBlobContentPropertiesToDbModel ( contentProperties ) { return { contentProperties : this . serializeModelValue ( { contentLength : contentProperties . contentLength , contentType : contentProperties . contentType , contentEncoding : contentProperties . contentEncoding , contentLanguage : contentProperties . contentLanguage , contentMD5 : contentProperties . contentMD5 , contentDisposition : contentProperties . contentDisposition , cacheControl : contentProperties . cacheControl } ) || null } ; } private convertDbModelToLease ( dbModel ) { const lease = ( this . deserializeModelValue ( dbModel , "lease" ) as ILease ) || { } ; if ( lease . leaseBreakTime && typeof lease . leaseBreakTime === "string" ) { lease . leaseBreakTime = new Date ( lease . leaseBreakTime ) ; } if ( lease . leaseExpireTime && typeof lease . leaseExpireTime === "string" ) { lease . leaseExpireTime = new Date ( lease . leaseExpireTime ) ; } return lease ; } private convertLeaseToDbModel ( lease ) { let leaseString = "" ; if ( lease instanceof ContainerLeaseAdapter || lease instanceof BlobLeaseAdapter ) { leaseString = lease . toString ( ) ; } else { leaseString = JSON . stringify ( lease ) ; } return { lease : leaseString } ; } private async getBlobWithLeaseUpdated ( account , container , blob , snapshot = "" , context , forceExist ? , forceCommitted ? , transaction ? ) { await this . checkContainerExist ( context , account , container ) ; const blobFindResult = await BlobsModel . findOne ( { where : { accountName : account , containerName : container , blobName : blob , snapshot } , transaction } ) ; if ( blobFindResult === null || blobFindResult === undefined ) { if ( forceExist === false ) { return undefined ; } else { throw StorageErrorFactory . getBlobNotFound ( context . contextId ) ; } } const doc = this . convertDbModelToBlobModel ( blobFindResult ) ; if ( forceExist === undefined || forceExist === true ) { if ( forceCommitted ) { if ( ! doc || ! ( doc as BlobModel ) . isCommitted ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ) ; } } else { if ( ! doc ) { throw StorageErrorFactory . getBlobNotFound ( context . contextId ) ; } } } else { if ( forceCommitted ) { if ( ! doc || ! ( doc as BlobModel ) . isCommitted ) { return undefined ; } } else { if ( ! doc ) { return undefined ; } } } if ( doc . properties ) { doc . properties . contentMD5 = this . restoreUint8Array ( doc . properties . contentMD5 ) ; } if ( snapshot !== undefined && snapshot !== "" ) { new BlobLeaseSyncer ( doc ) . sync ( { leaseId : undefined , leaseExpireTime : undefined , leaseDurationSeconds : undefined , leaseBreakTime : undefined , leaseDurationType : undefined , leaseState : Models . LeaseStateType . Available , leaseStatus : Models . LeaseStatusType . Unlocked } ) ; } else { LeaseFactory . createLeaseState ( new BlobLeaseAdapter ( doc ) , context ) . sync ( new BlobLeaseSyncer ( doc ) ) ; } return doc ; } private param { string } tier returns { ( Models . AccessTier | undefined ) } memberof private parseTier ( tier ) { tier = tier . toLowerCase ( ) ; if ( tier === Models . AccessTier . Hot . toLowerCase ( ) ) { return Models . AccessTier . Hot ; } if ( tier === Models . AccessTier . Cool . toLowerCase ( ) ) { return Models . AccessTier . Cool ; } if ( tier === Models . AccessTier . Archive . toLowerCase ( ) ) { return Models . AccessTier . Archive ; } return undefined ; } }