diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 000000000..fca24df4c --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "trailingComma": "none", + "tabWidth": 2, + "semi": true, + "singleQuote": false +} diff --git a/.vscode/settings.json b/.vscode/settings.json index e6cb6bba5..0143bafd2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "editor.tabSize": 2, - "editor.formatOnSave": true + "editor.formatOnSave": true, + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/ChangeLog.md b/ChangeLog.md index 62a983cc9..69573d74b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -2,6 +2,15 @@ > Note. This file includes changes after 3.0.0-preview. For legacy Azurite changes, please goto GitHub [releases](https://github.com/Azure/Azurite/releases). +## 2020.03 Version 3.6.0 + +- Supported conditional headers. +- Compatible with upper case or lower case of x-ms-sequence-number-action values. +- Fixed issue that x-ms-blob-sequence-number of 0 should be returned for HEAD requests on Page blob. +- Uploading blocks with different lengths of IDs to the same blob will fail. +- Check if block blob exists should fail if blocks are all uncommitted. +- Case sensitive with metadata keys. + ## 2020.02 Version 3.5.0 - Bump up Azure Storage service API version to 2019-07-07. diff --git a/README.md b/README.md index 740751820..aabee7c77 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ | Version | Azure Storage API Version | Service Support | Description | Reference Links | | ------------------------------------------------------------------ | ------------------------- | --------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 3.5.0 | 2019-07-07 | Blob
Queue | Azurite V3 based on TypeScript & New Architecture | [NPM](https://www.npmjs.com/package/azurite) - [Docker](https://hub.docker.com/_/microsoft-azure-storage-azurite) - [Visual Studio Code Extension](https://marketplace.visualstudio.com/items?itemName=Azurite.azurite) | +| 3.6.0 | 2019-07-07 | Blob
Queue | Azurite V3 based on TypeScript & New Architecture | [NPM](https://www.npmjs.com/package/azurite) - [Docker](https://hub.docker.com/_/microsoft-azure-storage-azurite) - [Visual Studio Code Extension](https://marketplace.visualstudio.com/items?itemName=Azurite.azurite) | | [Legacy (v2)](https://github.com/Azure/Azurite/tree/legacy-master) | 2016-05-31 | Blob, Queue and Table | Legacy Azurite V2 | [NPM](https://www.npmjs.com/package/azurite) | ## Introduction @@ -569,7 +569,7 @@ All the generated code is kept in `generated` folder, including the generated mi ## Support Matrix -3.5.0 release targets **2019-07-07** API version **blob** service. +Latest release targets **2019-07-07** API version **blob** service. Detailed support matrix: - Supported Vertical Features @@ -603,19 +603,19 @@ Detailed support matrix: - Snapshot Blob - Copy Blob (Only supports copy within same account in Azurite) - Abort Copy Blob (Only supports copy within same account in Azurite) + - Access control based on conditional headers - Following features or REST APIs are NOT supported or limited supported in this release (will support more features per customers feedback in future releases) - SharedKey Lite - OAuth authentication - - Access control based on conditional headers (Requests will be blocked in strict mode) - Static Website - Soft delete & Undelete Blob - Put Block from URL - Incremental Copy Blob - Create Append Blob, Append Block - 3.5.0 release added support for **2019-07-07** API version **queue** service. - Detailed support matrix: +Latest version supports for **2019-07-07** API version **queue** service. +Detailed support matrix: - Supported Vertical Features - SharedKey Authentication diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3111963e7..6491627fd 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -70,7 +70,7 @@ jobs: - job: blobtestmac displayName: Blob Test Mac pool: - vmImage: "macOS-10.13" + vmImage: "macOS-10.14" strategy: matrix: node_8_x: @@ -194,7 +194,7 @@ jobs: - job: queuetestmac displayName: Queue Test Mac pool: - vmImage: "macOS-10.13" + vmImage: "macOS-10.14" strategy: matrix: node_8_x: @@ -319,7 +319,7 @@ jobs: - job: azuritenodejsmac displayName: Azurite Mac pool: - vmImage: "macOS-10.13" + vmImage: "macOS-10.14" strategy: matrix: node_8_x: diff --git a/package-lock.json b/package-lock.json index 9df261c5b..f3fefdf34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "azurite", - "version": "3.5.0", + "version": "3.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -180,6 +180,22 @@ } } }, + "@mrmlnc/readdir-enhanced": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", + "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", + "dev": true, + "requires": { + "call-me-maybe": "^1.0.1", + "glob-to-regexp": "^0.3.0" + } + }, + "@nodelib/fs.stat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", + "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", + "dev": true + }, "@samverschueren/stream-to-observable": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz", @@ -1754,6 +1770,12 @@ "unset-value": "^1.0.0" } }, + "call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", + "dev": true + }, "caller-callsite": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", @@ -1869,6 +1891,97 @@ "string-width": "^1.0.1" } }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + } + } + }, "cls-bluebird": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-2.1.0.tgz", @@ -2151,6 +2264,12 @@ "ms": "2.0.0" } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", @@ -2283,6 +2402,16 @@ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, + "dir-glob": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz", + "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "path-type": "^3.0.0" + } + }, "dom-serializer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", @@ -2700,6 +2829,20 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, + "fast-glob": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", + "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", + "dev": true, + "requires": { + "@mrmlnc/readdir-enhanced": "^2.2.1", + "@nodelib/fs.stat": "^1.1.2", + "glob-parent": "^3.1.0", + "is-glob": "^4.0.0", + "merge2": "^1.2.3", + "micromatch": "^3.1.10" + } + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2873,6 +3016,12 @@ "is-property": "^1.0.2" } }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, "get-own-enumerable-property-symbols": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz", @@ -2921,6 +3070,33 @@ "path-is-absolute": "^1.0.0" } }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "glob-to-regexp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", + "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", + "dev": true + }, "globals": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", @@ -3138,6 +3314,12 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, "import-fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", @@ -3182,6 +3364,12 @@ "loose-envify": "^1.0.0" } }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, "ip-regex": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", @@ -3578,9 +3766,9 @@ } }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, "kuler": { @@ -3591,6 +3779,15 @@ "colornames": "^1.1.1" } }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, "leven": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", @@ -3831,6 +4028,15 @@ "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", "dev": true }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -3879,11 +4085,36 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + }, + "dependencies": { + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + } + } + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, + "merge2": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", + "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==", + "dev": true + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -4372,6 +4603,17 @@ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -4388,12 +4630,24 @@ "os-tmpdir": "^1.0.0" } }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", "dev": true }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true + }, "p-limit": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", @@ -4471,6 +4725,12 @@ "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", "dev": true }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -4505,6 +4765,15 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -4567,6 +4836,37 @@ "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", "dev": true }, + "prettier-tslint": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/prettier-tslint/-/prettier-tslint-0.4.2.tgz", + "integrity": "sha512-urhX7U/F+fu8sztEs/Z7CxNS8PdEytEwGKhQaH5fxxCdRmHGT45FoClyDlcZrMk9cK/8JpX/asFmTOHtSGJfLg==", + "dev": true, + "requires": { + "chalk": "^2.4.0", + "globby": "^8.0.1", + "ignore": "^3.3.7", + "require-relative": "^0.8.7", + "tslint": "^5.9.1", + "yargs": "^11.0.0" + }, + "dependencies": { + "globby": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.2.tgz", + "integrity": "sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w==", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "dir-glob": "2.0.0", + "fast-glob": "^2.0.2", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + } + } + } + }, "private": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", @@ -4819,6 +5119,24 @@ } } }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "require-relative": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", + "integrity": "sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=", + "dev": true + }, "resolve": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", @@ -5022,6 +5340,12 @@ "send": "0.17.1" } }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, "set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -5967,6 +6291,12 @@ "isexe": "^2.0.0" } }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, "winston": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", @@ -6092,11 +6422,130 @@ "resolved": "https://registry.npmjs.org/xpath.js/-/xpath.js-1.1.0.tgz", "integrity": "sha512-jg+qkfS4K8E7965sqaUl8mRngXiKb3WZGfONgE18pr03FUQiuSV6G+Ej4tS55B+rIQSFEIw3phdVAQ4pPqNWfQ==" }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, "yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" }, + "yargs": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.1.tgz", + "integrity": "sha512-PRU7gJrJaXv3q3yQZ/+/X6KBswZiaQ+zOmdprZcouPYtQgvNU35i+68M4b1ZHLZtYFT5QObFLV+ZkmJYcwKdiw==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.1.1", + "find-up": "^2.1.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^9.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "yargs-parser": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", + "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", + "dev": true, + "requires": { + "camelcase": "^4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + } + } + }, "yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", diff --git a/package.json b/package.json index be1fcf689..02a256e89 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Azurite", "description": "An open source Azure Storage API compatible server", "icon": "icon.png", - "version": "3.5.0", + "version": "3.6.0", "publisher": "Azurite", "categories": [ "Other" @@ -37,7 +37,6 @@ "xml2js": "^0.4.19" }, "devDependencies": { - "@types/validator": "^10.11.3", "@azure/storage-blob": "^10.4.1", "@azure/storage-queue": "^10.2.0", "@types/args": "^3.0.0", @@ -53,6 +52,7 @@ "@types/rimraf": "^2.0.2", "@types/uri-templates": "^0.1.29", "@types/uuid": "^3.4.4", + "@types/validator": "^10.11.3", "@types/vscode": "^1.39.0", "@types/xml2js": "^0.4.3", "autorest": "^2.0.4283", @@ -62,6 +62,7 @@ "lint-staged": "^8.1.5", "mocha": "^5.2.0", "prettier": "^1.16.4", + "prettier-tslint": "^0.4.2", "ts-node": "^7.0.1", "tslint": "^5.16.0", "typescript": "^3.1.4", diff --git a/src/blob/authentication/BlobTokenAuthenticator.ts b/src/blob/authentication/BlobTokenAuthenticator.ts new file mode 100644 index 000000000..90d256691 --- /dev/null +++ b/src/blob/authentication/BlobTokenAuthenticator.ts @@ -0,0 +1,68 @@ +import IAccountDataStore from "../../common/IAccountDataStore"; +import ILogger from "../../common/ILogger"; +import BlobStorageContext from "../context/BlobStorageContext"; +import StorageErrorFactory from "../errors/StorageErrorFactory"; +import Context from "../generated/Context"; +import IRequest from "../generated/IRequest"; +import { HeaderConstants } from "../utils/constants"; +import IAuthenticator from "./IAuthenticator"; + +export default class BlobTokenAuthenticator implements IAuthenticator { + public constructor( + private readonly dataStore: IAccountDataStore, + private readonly logger: ILogger + ) {} + + public async validate( + req: IRequest, + context: Context + ): Promise { + const blobContext = new BlobStorageContext(context); + const account = blobContext.account!; + + this.logger.info( + `BlobTokenAuthenticator:validate() Start validation against token authentication.`, + blobContext.contextId + ); + + // TODO: Make following async + const accountProperties = this.dataStore.getAccount(account); + if (accountProperties === undefined) { + this.logger.error( + `BlobTokenAuthenticator:validate() Invalid storage account ${account}.`, + blobContext.contextId + ); + throw StorageErrorFactory.getInvalidOperation( + blobContext.contextId!, + "Invalid storage account." + ); + } + + const authHeaderValue = req.getHeader(HeaderConstants.AUTHORIZATION); + if (authHeaderValue === undefined) { + this.logger.info( + // tslint:disable-next-line:max-line-length + `BlobTokenAuthenticator:validate() Request doesn't include valid authentication header. Skip token authentication.`, + blobContext.contextId + ); + return undefined; + } else { + const hasBearerToken = authHeaderValue.startsWith("Bearer"); + + if (hasBearerToken) { + this.logger.info( + // tslint:disable-next-line:max-line-length + `BlobTokenAuthenticator:validate() Request includes Bearer token.`, + blobContext.contextId + ); + } else { + this.logger.info( + // tslint:disable-next-line:max-line-length + `BlobTokenAuthenticator:validate() Request does not include Bearer token. Skip token authentication.`, + blobContext.contextId + ); + } + return hasBearerToken; + } + } +} diff --git a/src/blob/conditions/ConditionResourceAdapter.ts b/src/blob/conditions/ConditionResourceAdapter.ts new file mode 100644 index 000000000..2cc0144db --- /dev/null +++ b/src/blob/conditions/ConditionResourceAdapter.ts @@ -0,0 +1,37 @@ +import { BlobModel, ContainerModel } from "../persistence/IBlobMetadataStore"; +import IConditionResource from "./IConditionResource"; + +export default class ConditionResourceAdapter implements IConditionResource { + public exist: boolean; + public etag: string; + public lastModified: Date; + + public constructor(resource: BlobModel | ContainerModel | undefined | null) { + if ( + resource === undefined || + resource === null || + (resource as BlobModel).isCommitted === false // Treat uncommitted blob as unexist resource + ) { + this.exist = false; + this.etag = "UNEXIST_RESOURCE_ETAG"; + this.lastModified = undefined as any; + return; + } + + this.exist = true; + this.etag = resource.properties.etag; + + if (this.etag.length < 3) { + throw new Error( + `ConditionResourceAdapter::constructor() Invalid etag ${this.etag}.` + ); + } + + if (this.etag.startsWith('"') && this.etag.endsWith('"')) { + this.etag = this.etag.substring(1, this.etag.length - 1); + } + + this.lastModified = new Date(resource.properties.lastModified); + this.lastModified.setMilliseconds(0); // Precision to seconds + } +} diff --git a/src/blob/conditions/ConditionalHeadersAdapter.ts b/src/blob/conditions/ConditionalHeadersAdapter.ts new file mode 100644 index 000000000..40ef469ec --- /dev/null +++ b/src/blob/conditions/ConditionalHeadersAdapter.ts @@ -0,0 +1,47 @@ +import { ModifiedAccessConditions } from "../generated/artifacts/models"; +import Context from "../generated/Context"; +import { IConditionalHeaders } from "./IConditionalHeaders"; + +export default class ConditionalHeadersAdapter implements IConditionalHeaders { + public ifModifiedSince?: Date; + public ifUnmodifiedSince?: Date; + public ifMatch?: string[]; + public ifNoneMatch?: string[]; + + public constructor( + context: Context, + modifiedAccessConditions: ModifiedAccessConditions = {} + ) { + // If-Match & If-None-Match allow multi values separated by comma + if (modifiedAccessConditions.ifMatch) { + this.ifMatch = modifiedAccessConditions.ifMatch.split(",").map(etag => { + if (etag.startsWith('"') && etag.endsWith('"')) { + return etag.substring(1, etag.length - 1); + } + return etag; + }); + } + + if (modifiedAccessConditions.ifNoneMatch) { + this.ifNoneMatch = modifiedAccessConditions.ifNoneMatch + .split(",") + .map(etag => { + if (etag.startsWith('"') && etag.endsWith('"')) { + return etag.substring(1, etag.length - 1); + } + return etag; + }); + } + + // If-Modified-Since & If-Unmodified-Since don't support multi values + this.ifModifiedSince = modifiedAccessConditions.ifModifiedSince; + if (this.ifModifiedSince) { + this.ifModifiedSince.setMilliseconds(0); // Precision to seconds + } + + this.ifUnmodifiedSince = modifiedAccessConditions.ifUnmodifiedSince; + if (this.ifUnmodifiedSince) { + this.ifUnmodifiedSince.setMilliseconds(0); // Precision to seconds + } + } +} diff --git a/src/blob/conditions/IConditionResource.ts b/src/blob/conditions/IConditionResource.ts new file mode 100644 index 000000000..dda91509c --- /dev/null +++ b/src/blob/conditions/IConditionResource.ts @@ -0,0 +1,16 @@ +export default interface IConditionResource { + /** + * Whether resource exists or not. + */ + exist: boolean; + + /** + * etag string without quotes. + */ + etag: string; + + /** + * last modified time for container or blob. + */ + lastModified: Date; +} diff --git a/src/blob/conditions/IConditionalHeaders.ts b/src/blob/conditions/IConditionalHeaders.ts new file mode 100644 index 000000000..1749fca54 --- /dev/null +++ b/src/blob/conditions/IConditionalHeaders.ts @@ -0,0 +1,14 @@ +export interface IConditionalHeaders { + ifModifiedSince?: Date; + ifUnmodifiedSince?: Date; + + /** + * If-Match etag list without quotes. + */ + ifMatch?: string[]; + + /** + * If-None-Match etag list without quotes. + */ + ifNoneMatch?: string[]; +} diff --git a/src/blob/conditions/IConditionalHeadersValidator.ts b/src/blob/conditions/IConditionalHeadersValidator.ts new file mode 100644 index 000000000..7a8c04431 --- /dev/null +++ b/src/blob/conditions/IConditionalHeadersValidator.ts @@ -0,0 +1,11 @@ +import Context from "../generated/Context"; +import { IConditionalHeaders } from "./IConditionalHeaders"; +import IConditionResource from "./IConditionResource"; + +export interface IConditionalHeadersValidator { + validate( + context: Context, + conditionalHeaders: IConditionalHeaders, + resource: IConditionResource + ): void; +} diff --git a/src/blob/conditions/ReadConditionalHeadersValidator.ts b/src/blob/conditions/ReadConditionalHeadersValidator.ts new file mode 100644 index 000000000..6943d5f5e --- /dev/null +++ b/src/blob/conditions/ReadConditionalHeadersValidator.ts @@ -0,0 +1,112 @@ +import StorageErrorFactory from "../errors/StorageErrorFactory"; +import { ModifiedAccessConditions } from "../generated/artifacts/models"; +import Context from "../generated/Context"; +import { BlobModel, ContainerModel } from "../persistence/IBlobMetadataStore"; +import ConditionalHeadersAdapter from "./ConditionalHeadersAdapter"; +import ConditionResourceAdapter from "./ConditionResourceAdapter"; +import { IConditionalHeaders } from "./IConditionalHeaders"; +import { IConditionalHeadersValidator } from "./IConditionalHeadersValidator"; +import IConditionResource from "./IConditionResource"; + +export function validateReadConditions( + context: Context, + conditionalHeaders?: ModifiedAccessConditions, + model?: BlobModel | ContainerModel | null +) { + new ReadConditionalHeadersValidator().validate( + context, + new ConditionalHeadersAdapter(context, conditionalHeaders), + new ConditionResourceAdapter(model) + ); +} + +// tslint:disable: max-line-length +export default class ReadConditionalHeadersValidator + implements IConditionalHeadersValidator { + /** + * Validate Conditional Headers for Blob Service Read Operations in Version 2013-08-15 or Later. + * @link https://docs.microsoft.com/en-us/rest/api/storageservices/specifying-conditional-headers-for-blob-service-operations#specifying-conditional-headers-for-blob-service-read-operations-in-version-2013-08-15-or-later + * + * @param context + * @param conditionalHeaders + * @param resource + */ + public validate( + context: Context, + conditionalHeaders: IConditionalHeaders, + resource: IConditionResource + ): void { + // If-Match && If-Unmodified-Since && (If-None-Match || If-Modified-Since) + + // Read against a non exist resource + if (!resource.exist) { + // If-Match + if (conditionalHeaders.ifMatch && conditionalHeaders.ifMatch.length > 0) { + throw StorageErrorFactory.getConditionNotMet(context.contextId!); + } + + // If If-Unmodified-Since + // Skip for unexist resource + + // If-None-Match + if ( + conditionalHeaders.ifNoneMatch && + conditionalHeaders.ifNoneMatch.length > 0 && + conditionalHeaders.ifNoneMatch[0] === "*" + ) { + throw StorageErrorFactory.getUnsatisfiableCondition(context.contextId!); + } + + // If-Modified-Since + // Skip for unexist resource + } else { + // Read against an existing resource + // If-Match && If-Unmodified-Since && (If-None-Match || If-Modified-Since) + + // If-Match + const ifMatchPass = conditionalHeaders.ifMatch + ? conditionalHeaders.ifMatch.includes(resource.etag) || + conditionalHeaders.ifMatch[0] === "*" + : undefined; + + // If-Unmodified-Since + const ifUnModifiedSincePass = conditionalHeaders.ifUnmodifiedSince + ? resource.lastModified <= conditionalHeaders.ifUnmodifiedSince + : undefined; + + // If-None-Match + if ( + conditionalHeaders.ifNoneMatch && + conditionalHeaders.ifNoneMatch.length > 0 && + conditionalHeaders.ifNoneMatch[0] === "*" + ) { + throw StorageErrorFactory.getUnsatisfiableCondition(context.contextId!); + } + + const ifNoneMatchPass = conditionalHeaders.ifNoneMatch + ? !conditionalHeaders.ifNoneMatch.includes(resource.etag) + : undefined; + + // If-Modified-Since + const isModifiedSincePass = conditionalHeaders.ifModifiedSince + ? conditionalHeaders.ifModifiedSince < resource.lastModified + : undefined; + + if (ifMatchPass === false) { + throw StorageErrorFactory.getConditionNotMet(context.contextId!); + } + + if (ifUnModifiedSincePass === false) { + throw StorageErrorFactory.getConditionNotMet(context.contextId!); + } + + if (ifNoneMatchPass === false && isModifiedSincePass !== true) { + throw StorageErrorFactory.getNotModified(context.contextId!); + } + + if (isModifiedSincePass === false && ifNoneMatchPass !== true) { + throw StorageErrorFactory.getNotModified(context.contextId!); + } + } + } +} diff --git a/src/blob/conditions/WriteConditionalHeadersValidator.ts b/src/blob/conditions/WriteConditionalHeadersValidator.ts new file mode 100644 index 000000000..15cf67728 --- /dev/null +++ b/src/blob/conditions/WriteConditionalHeadersValidator.ts @@ -0,0 +1,235 @@ +import StorageErrorFactory from "../errors/StorageErrorFactory"; +import { + ModifiedAccessConditions, + SequenceNumberAccessConditions +} from "../generated/artifacts/models"; +import Context from "../generated/Context"; +import { BlobModel, ContainerModel } from "../persistence/IBlobMetadataStore"; +import ConditionalHeadersAdapter from "./ConditionalHeadersAdapter"; +import ConditionResourceAdapter from "./ConditionResourceAdapter"; +import { IConditionalHeaders } from "./IConditionalHeaders"; +import { IConditionalHeadersValidator } from "./IConditionalHeadersValidator"; +import IConditionResource from "./IConditionResource"; + +export function validateSequenceNumberWriteConditions( + context: Context, + conditionalHeaders?: SequenceNumberAccessConditions, + model?: BlobModel +) { + if (!conditionalHeaders || !model) { + return; + } + + if (!model.properties || model.properties.blobSequenceNumber === undefined) { + throw Error( + `validateSequenceNumberWriteConditions() Invalid blob model, blobSequenceNumber is not specified.` + ); + } + + if ( + conditionalHeaders.ifSequenceNumberLessThanOrEqualTo !== undefined && + conditionalHeaders.ifSequenceNumberLessThanOrEqualTo < + model.properties.blobSequenceNumber + ) { + throw StorageErrorFactory.getSequenceNumberConditionNotMet( + context.contextId! + ); + } + + if ( + conditionalHeaders.ifSequenceNumberLessThan !== undefined && + conditionalHeaders.ifSequenceNumberLessThan <= + model.properties.blobSequenceNumber + ) { + throw StorageErrorFactory.getSequenceNumberConditionNotMet( + context.contextId! + ); + } + + if ( + conditionalHeaders.ifSequenceNumberEqualTo !== undefined && + conditionalHeaders.ifSequenceNumberEqualTo !== + model.properties.blobSequenceNumber + ) { + throw StorageErrorFactory.getSequenceNumberConditionNotMet( + context.contextId! + ); + } +} + +export function validateWriteConditions( + context: Context, + conditionalHeaders?: ModifiedAccessConditions, + model?: BlobModel | ContainerModel | null +) { + new WriteConditionalHeadersValidator().validate( + context, + new ConditionalHeadersAdapter(context, conditionalHeaders), + new ConditionResourceAdapter(model) + ); +} + +// tslint:disable: max-line-length +export default class WriteConditionalHeadersValidator + implements IConditionalHeadersValidator { + /** + * Validate conditional Headers for Read Operations in Versions Prior to 2013-08-15, + * and for Write Operations (All Versions). + * @link https://docs.microsoft.com/en-us/rest/api/storageservices/specifying-conditional-headers-for-blob-service-operations#specifying-conditional-headers-for-read-operations-in-versions-prior-to-2013-08-15-and-for-write-operations-all-versions + * + * @param context + * @param conditionalHeaders + * @param resource + */ + public validate( + context: Context, + conditionalHeaders: IConditionalHeaders, + resource: IConditionResource + ): void { + this.validateCombinations(context, conditionalHeaders); + if (!resource.exist) { + if ( + conditionalHeaders.ifNoneMatch && + conditionalHeaders.ifNoneMatch.length > 0 + ) { + // If a request specifies both the If-None-Match and If-Modified-Since headers, + // the request is evaluated based on the criteria specified in If-None-Match. + // Skip for non exist blob + return; + } + + if (conditionalHeaders.ifMatch && conditionalHeaders.ifMatch.length > 0) { + // If a request specifies both the If-Match and If-Unmodified-Since headers, + // the request is evaluated based on the criteria specified in If-Match. + if (conditionalHeaders.ifMatch[0] !== "*") { + throw StorageErrorFactory.getConditionNotMet(context.contextId!); + } + return; + } + + if (conditionalHeaders.ifModifiedSince) { + // Skip for non exist blob + return; + } + + if (conditionalHeaders.ifUnmodifiedSince) { + // Skip for non exist blob + return; + } + } else { + if ( + conditionalHeaders.ifNoneMatch && + conditionalHeaders.ifNoneMatch.length > 0 + ) { + if (conditionalHeaders.ifNoneMatch[0] === "*") { + // According to restful doc, specify the wildcard character (*) to perform the operation + // only if the resource does not exist, and fail the operation if it does exist. + // However, Azure Storage Set Blob Properties Operation for an existing blob doesn't reuturn 412 with * + // TODO: Check accurate behavior for different write operations + // Put Blob, Commit Block List has special logic for ifNoneMatch equals *, will return 409 conflict for existing blob, will handled in createBlob metatdata store. + // throw StorageErrorFactory.getConditionNotMet(context.contextId!); + return; + } + if (conditionalHeaders.ifNoneMatch[0] === resource.etag) { + throw StorageErrorFactory.getConditionNotMet(context.contextId!); + } + + // Stop processing + // If a request specifies both the If-None-Match and If-Modified-Since headers, + // the request is evaluated based on the criteria specified in If-None-Match. + return; + } + + if (conditionalHeaders.ifMatch && conditionalHeaders.ifMatch.length > 0) { + if ( + conditionalHeaders.ifMatch[0] !== "*" && + conditionalHeaders.ifMatch[0] !== resource.etag + ) { + throw StorageErrorFactory.getConditionNotMet(context.contextId!); + } + + // Stop processing + // If a request specifies both the If-Match and If-Unmodified-Since headers, + // the request is evaluated based on the criteria specified in If-Match. + return; + } + + if (conditionalHeaders.ifModifiedSince) { + if (resource.lastModified <= conditionalHeaders.ifModifiedSince) { + throw StorageErrorFactory.getConditionNotMet(context.contextId!); + } + return; + } + + if (conditionalHeaders.ifUnmodifiedSince) { + if (conditionalHeaders.ifUnmodifiedSince < resource.lastModified) { + throw StorageErrorFactory.getConditionNotMet(context.contextId!); + } + return; + } + } + } + + private validateCombinations( + context: Context, + conditionalHeaders: IConditionalHeaders + ) { + let ifMatch = 0; + if (conditionalHeaders.ifMatch && conditionalHeaders.ifMatch.length > 0) { + // RFC 2616 allows multiple ETag values in a single header, + // but requests to the Blob service can only include one ETag value. + // Specifying more than one ETag value results in status code 400 (Bad Request). + if (conditionalHeaders.ifMatch.length > 1) { + // throw 400 MultipleConditionHeadersNotSupported Multiple condition headers are not supported. + throw StorageErrorFactory.getMultipleConditionHeadersNotSupported( + context.contextId! + ); + } + + ifMatch = 1; + } + + let ifModifiedSince = 0; + if (conditionalHeaders.ifModifiedSince) { + ifModifiedSince = 1; + } + + let ifNoneMatch = 0; + if ( + conditionalHeaders.ifNoneMatch && + conditionalHeaders.ifNoneMatch.length > 0 + ) { + // RFC 2616 allows multiple ETag values in a single header, + // but requests to the Blob service can only include one ETag value. + // Specifying more than one ETag value results in status code 400 (Bad Request). + if (conditionalHeaders.ifNoneMatch.length > 1) { + // throw 400 MultipleConditionHeadersNotSupported Multiple condition headers are not supported. + throw StorageErrorFactory.getMultipleConditionHeadersNotSupported( + context.contextId! + ); + } + ifNoneMatch = 1; + } + + let ifUnmodifiedSince = 0; + if (conditionalHeaders.ifUnmodifiedSince) { + ifUnmodifiedSince = 1; + } + + if (ifMatch + ifModifiedSince + ifNoneMatch + ifUnmodifiedSince > 2) { + // throw 400 MultipleConditionHeadersNotSupported Multiple condition headers are not supported. + throw StorageErrorFactory.getMultipleConditionHeadersNotSupported( + context.contextId! + ); + } + + if (ifMatch + ifModifiedSince + ifNoneMatch + ifUnmodifiedSince === 2) { + if (ifNoneMatch + ifModifiedSince === 1) { + // throw 400 MultipleConditionHeadersNotSupported Multiple condition headers are not supported. + throw StorageErrorFactory.getMultipleConditionHeadersNotSupported( + context.contextId! + ); + } + } + } +} diff --git a/src/blob/errors/StorageError.ts b/src/blob/errors/StorageError.ts index 00ef46fac..a1a24d899 100644 --- a/src/blob/errors/StorageError.ts +++ b/src/blob/errors/StorageError.ts @@ -9,6 +9,10 @@ import { jsonToXML } from "../generated/utils/xml"; * @extends {MiddlewareError} */ export default class StorageError extends MiddlewareError { + public readonly storageErrorCode: string; + public readonly storageErrorMessage: string; + public readonly storageRequestID: string; + /** * Creates an instance of StorageError. * @@ -54,5 +58,8 @@ export default class StorageError extends MiddlewareError { ); this.name = "StorageError"; + this.storageErrorCode = storageErrorCode; + this.storageErrorMessage = storageErrorMessage; + this.storageRequestID = storageRequestID; } } diff --git a/src/blob/errors/StorageErrorFactory.ts b/src/blob/errors/StorageErrorFactory.ts index 3f394ebf1..38f97dcce 100644 --- a/src/blob/errors/StorageErrorFactory.ts +++ b/src/blob/errors/StorageErrorFactory.ts @@ -31,6 +31,17 @@ export default class StorageErrorFactory { ); } + public static getBlobAlreadyExists( + contextID: string = DefaultID + ): StorageError { + return new StorageError( + 409, + "BlobAlreadyExists", + "The specified blob already exists.", + contextID + ); + } + public static getBlobNotFound(contextID: string = DefaultID): StorageError { return new StorageError( 404, @@ -160,6 +171,17 @@ export default class StorageErrorFactory { ); } + public static getInvalidBlobOrBlock( + contextID: string = DefaultID + ): StorageError { + return new StorageError( + 400, + "InvalidBlobOrBlock", + "The specified blob or block content is invalid.", + contextID + ); + } + public static getLeaseAlreadyPresent( contextID: string = DefaultID ): StorageError { @@ -354,6 +376,17 @@ export default class StorageErrorFactory { ); } + public static getMultipleConditionHeadersNotSupported( + contextID: string + ): StorageError { + return new StorageError( + 400, + "MultipleConditionHeadersNotSupported", + "Multiple condition headers are not supported.", + contextID + ); + } + public static getBlobSnapshotsPresent_hassnapshot( contextID: string ): StorageError { @@ -478,6 +511,44 @@ export default class StorageErrorFactory { ); } + public static getConditionNotMet(contextID: string): StorageError { + return new StorageError( + 412, + "ConditionNotMet", + "The condition specified using HTTP conditional header(s) is not met.", + contextID + ); + } + + public static getSequenceNumberConditionNotMet( + contextID: string + ): StorageError { + return new StorageError( + 412, + "SequenceNumberConditionNotMet", + "The condition specified using HTTP conditional header(s) is not met.", + contextID + ); + } + + public static getNotModified(contextID: string): StorageError { + return new StorageError( + 304, + "ConditionNotMet", + "The condition specified using HTTP conditional header(s) is not met.", + contextID + ); + } + + public static getUnsatisfiableCondition(contextID: string): StorageError { + return new StorageError( + 400, + "UnsatisfiableCondition", + "The request includes an unsatisfiable condition for this operation.", + contextID + ); + } + public static getInvalidHeaderValue( contextID: string = "", additionalMessages?: { [key: string]: string } diff --git a/src/blob/generated/ExpressRequestAdapter.ts b/src/blob/generated/ExpressRequestAdapter.ts index dced6a1ec..c37abcb31 100644 --- a/src/blob/generated/ExpressRequestAdapter.ts +++ b/src/blob/generated/ExpressRequestAdapter.ts @@ -43,6 +43,10 @@ export default class ExpressRequestAdapter implements IRequest { return this.req.headers; } + public getRawHeaders(): string[] { + return this.req.rawHeaders; + } + public getQuery(key: string): string | undefined { return this.req.query[key]; } diff --git a/src/blob/generated/IRequest.ts b/src/blob/generated/IRequest.ts index 0fb3060ea..df39dd0ad 100644 --- a/src/blob/generated/IRequest.ts +++ b/src/blob/generated/IRequest.ts @@ -19,6 +19,7 @@ export default interface IRequest { getBody(): string | undefined; getHeader(field: string): string | undefined; getHeaders(): { [header: string]: string | string[] | undefined }; + getRawHeaders(): string[]; getQuery(key: string): string | undefined; getProtocol(): string; } diff --git a/src/blob/handlers/BlobHandler.ts b/src/blob/handlers/BlobHandler.ts index 7f2738579..5d38f9527 100644 --- a/src/blob/handlers/BlobHandler.ts +++ b/src/blob/handlers/BlobHandler.ts @@ -1,6 +1,7 @@ import { URL } from "url"; import IExtentStore from "../../common/persistence/IExtentStore"; +import { convertRawHeadersToMetadata } from "../../common/utils/utils"; import BlobStorageContext from "../context/BlobStorageContext"; import NotImplementedError from "../errors/NotImplementedError"; import StorageErrorFactory from "../errors/StorageErrorFactory"; @@ -15,7 +16,8 @@ import IBlobMetadataStore, { import { BLOB_API_VERSION, EMULATOR_ACCOUNT_KIND, - EMULATOR_ACCOUNT_SKUNAME + EMULATOR_ACCOUNT_SKUNAME, + HeaderConstants } from "../utils/constants"; import { deserializePageBlobRangeHeader, @@ -96,7 +98,8 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler { containerName, blobName, options.snapshot, - options.leaseAccessConditions + options.leaseAccessConditions, + options.modifiedAccessConditions ); if (blob.properties.blobType === Models.BlobType.BlockBlob) { @@ -133,7 +136,8 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler { container, blob, options.snapshot, - options.leaseAccessConditions + options.leaseAccessConditions, + options.modifiedAccessConditions ); // TODO: Create get metadata specific request in swagger @@ -236,16 +240,43 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler { const account = blobCtx.account!; const container = blobCtx.container!; const blob = blobCtx.blob!; - const res = await this.metadataStore.setBlobHTTPHeaders( - context, - account, - container, - blob, - options.leaseAccessConditions, - options.blobHTTPHeaders + + let res; + + // Workaround for https://github.com/Azure/Azurite/issues/332 + const sequenceNumberAction = context.request!.getHeader( + HeaderConstants.X_MS_SEQUENCE_NUMBER_ACTION + ); + const sequenceNumber = context.request!.getHeader( + HeaderConstants.X_MS_BLOB_SEQUENCE_NUMBER ); + if (sequenceNumberAction !== undefined) { + this.logger.verbose( + "BlobHandler:setHTTPHeaders() Redirect to updateSequenceNumber...", + context.contextId + ); + res = await this.metadataStore.updateSequenceNumber( + context, + account, + container, + blob, + sequenceNumberAction.toLowerCase() as Models.SequenceNumberActionType, + sequenceNumber === undefined ? undefined : parseInt(sequenceNumber, 10), + options.leaseAccessConditions, + options.modifiedAccessConditions + ); + } else { + res = await this.metadataStore.setBlobHTTPHeaders( + context, + account, + container, + blob, + options.leaseAccessConditions, + options.blobHTTPHeaders, + options.modifiedAccessConditions + ); + } - // ToDo: return correct headers and test for these. const response: Models.BlobSetHTTPHeadersResponse = { statusCode: 200, eTag: res.etag, @@ -276,13 +307,20 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler { const account = blobCtx.account!; const container = blobCtx.container!; const blob = blobCtx.blob!; + + // Preserve metadata key case + const metadata = convertRawHeadersToMetadata( + blobCtx.request!.getRawHeaders() + ); + const res = await this.metadataStore.setBlobMetadata( context, account, container, blob, options.leaseAccessConditions, - options.metadata + metadata, + options.modifiedAccessConditions ); // ToDo: return correct headers and test for these. @@ -331,7 +369,8 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler { container, blob, options.duration!, - options.proposedLeaseId + options.proposedLeaseId, + options ); const response: Models.BlobAcquireLeaseResponse = { @@ -371,7 +410,8 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler { account, container, blob, - leaseId + leaseId, + options ); const response: Models.BlobReleaseLeaseResponse = { @@ -410,7 +450,8 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler { account, container, blob, - leaseId + leaseId, + options ); const response: Models.BlobRenewLeaseResponse = { @@ -453,7 +494,8 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler { container, blob, leaseId, - proposedLeaseId + proposedLeaseId, + options ); const response: Models.BlobChangeLeaseResponse = { @@ -491,7 +533,8 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler { account, container, blob, - options.breakPeriod + options.breakPeriod, + options ); const response: Models.BlobBreakLeaseResponse = { @@ -526,6 +569,12 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler { const account = blobCtx.account!; const container = blobCtx.container!; const blob = blobCtx.blob!; + + // Preserve metadata key case + const metadata = convertRawHeadersToMetadata( + blobCtx.request!.getRawHeaders() + ); + const res = await this.metadataStore.createSnapshot( context, account, @@ -534,7 +583,8 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler { options.leaseAccessConditions, !options.metadata || JSON.stringify(options.metadata) === "{}" ? undefined - : options.metadata + : metadata, + options.modifiedAccessConditions ); const response: Models.BlobCreateSnapshotResponse = { @@ -576,7 +626,7 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler { sourceAccount, sourceContainer, sourceBlob - ] = extractStoragePartsFromPath(url.pathname); + ] = extractStoragePartsFromPath(url.hostname, url.pathname); const snapshot = url.searchParams.get("snapshot") || ""; if ( @@ -588,6 +638,11 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler { throw StorageErrorFactory.getBlobNotFound(context.contextId!); } + // Preserve metadata key case + const metadata = convertRawHeadersToMetadata( + blobCtx.request!.getRawHeaders() + ); + const res = await this.metadataStore.startCopyFromURL( context, { @@ -598,9 +653,9 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler { }, { account, container, blob }, copySource, - options.metadata, + metadata, options.tier, - options.leaseAccessConditions + options ); const response: Models.BlobStartCopyFromURLResponse = { diff --git a/src/blob/handlers/BlockBlobHandler.ts b/src/blob/handlers/BlockBlobHandler.ts index 0953b43fa..55157bc44 100644 --- a/src/blob/handlers/BlockBlobHandler.ts +++ b/src/blob/handlers/BlockBlobHandler.ts @@ -1,3 +1,4 @@ +import { convertRawHeadersToMetadata } from "../../common/utils/utils"; import BlobStorageContext from "../context/BlobStorageContext"; import NotImplementedError from "../errors/NotImplementedError"; import StorageErrorFactory from "../errors/StorageErrorFactory"; @@ -89,7 +90,8 @@ export default class BlockBlobHandler extends BaseHandler const blob: BlobModel = { deleted: false, - metadata: options.metadata, + // Preserve metadata key case + metadata: convertRawHeadersToMetadata(blobCtx.request!.getRawHeaders()), accountName, containerName, name: blobName, @@ -131,7 +133,8 @@ export default class BlockBlobHandler extends BaseHandler await this.metadataStore.createBlob( context, blob, - options.leaseAccessConditions + options.leaseAccessConditions, + options.modifiedAccessConditions ); const response: Models.BlockBlobUploadResponse = { @@ -284,7 +287,10 @@ export default class BlockBlobHandler extends BaseHandler }; blob.properties.blobType = Models.BlobType.BlockBlob; - blob.metadata = options.metadata; + blob.metadata = convertRawHeadersToMetadata( + // Preserve metadata key case + blobCtx.request!.getRawHeaders() + ); blob.properties.accessTier = Models.AccessTier.Hot; blob.properties.cacheControl = options.blobHTTPHeaders.blobCacheControl; blob.properties.contentType = contentType; @@ -313,7 +319,8 @@ export default class BlockBlobHandler extends BaseHandler context, blob, commitBlockList, - options.leaseAccessConditions + options.leaseAccessConditions, + options.modifiedAccessConditions ); const contentMD5 = await getMD5FromString(rawBody); @@ -352,6 +359,8 @@ export default class BlockBlobHandler extends BaseHandler ); // TODO: Create uncommitted blockblob when stage block + // TODO: Conditional headers support? + res.properties = res.properties || {}; const response: Models.BlockBlobGetBlockListResponse = { statusCode: 200, diff --git a/src/blob/handlers/ContainerHandler.ts b/src/blob/handlers/ContainerHandler.ts index 6486997f3..d2a729914 100644 --- a/src/blob/handlers/ContainerHandler.ts +++ b/src/blob/handlers/ContainerHandler.ts @@ -1,3 +1,4 @@ +import { convertRawHeadersToMetadata } from "../../common/utils/utils"; import BlobStorageContext from "../context/BlobStorageContext"; import * as Models from "../generated/artifacts/models"; import Context from "../generated/Context"; @@ -38,10 +39,15 @@ export default class ContainerHandler extends BaseHandler const lastModified = blobCtx.startTime!; const etag = newEtag(); + // Preserve metadata key case + const metadata = convertRawHeadersToMetadata( + blobCtx.request!.getRawHeaders() + ); + await this.metadataStore.createContainer(context, { accountName, name: containerName, - metadata: options.metadata, + metadata, properties: { etag, lastModified, @@ -142,7 +148,7 @@ export default class ContainerHandler extends BaseHandler context, accountName, containerName, - options.leaseAccessConditions + options ); const response: Models.ContainerDeleteResponse = { @@ -174,14 +180,20 @@ export default class ContainerHandler extends BaseHandler const date = blobCtx.startTime!; const eTag = newEtag(); + // Preserve metadata key case + const metadata = convertRawHeadersToMetadata( + blobCtx.request!.getRawHeaders() + ); + await this.metadataStore.setContainerMetadata( context, accountName, containerName, date, eTag, - options.metadata, - options.leaseAccessConditions + metadata, + options.leaseAccessConditions, + options.modifiedAccessConditions ); const response: Models.ContainerSetMetadataResponse = { @@ -271,7 +283,8 @@ export default class ContainerHandler extends BaseHandler etag: eTag, publicAccess: options.access, containerAcl: options.containerAcl, - leaseAccessConditions: options.leaseAccessConditions + leaseAccessConditions: options.leaseAccessConditions, + modifiedAccessConditions: options.modifiedAccessConditions } ); @@ -351,7 +364,8 @@ export default class ContainerHandler extends BaseHandler context, accountName, containerName, - leaseId + leaseId, + options ); const response: Models.ContainerReleaseLeaseResponse = { @@ -391,7 +405,8 @@ export default class ContainerHandler extends BaseHandler context, accountName, containerName, - leaseId + leaseId, + options ); const response: Models.ContainerRenewLeaseResponse = { @@ -430,7 +445,8 @@ export default class ContainerHandler extends BaseHandler context, accountName, containerName, - options.breakPeriod + options.breakPeriod, + options ); const response: Models.ContainerBreakLeaseResponse = { @@ -474,7 +490,8 @@ export default class ContainerHandler extends BaseHandler accountName, containerName, leaseId, - proposedLeaseId + proposedLeaseId, + options ); const response: Models.ContainerChangeLeaseResponse = { diff --git a/src/blob/handlers/PageBlobHandler.ts b/src/blob/handlers/PageBlobHandler.ts index 85f6c87df..1d6cf8e60 100644 --- a/src/blob/handlers/PageBlobHandler.ts +++ b/src/blob/handlers/PageBlobHandler.ts @@ -1,4 +1,5 @@ import IExtentStore from "../../common/persistence/IExtentStore"; +import { convertRawHeadersToMetadata } from "../../common/utils/utils"; import BlobStorageContext from "../context/BlobStorageContext"; import NotImplementedError from "../errors/NotImplementedError"; import StorageErrorFactory from "../errors/StorageErrorFactory"; @@ -96,10 +97,15 @@ export default class PageBlobHandler extends BaseHandler // ); // } + // Preserve metadata key case + const metadata = convertRawHeadersToMetadata( + blobCtx.request!.getRawHeaders() + ); + const etag = newEtag(); const blob: BlobModel = { deleted: false, - metadata: options.metadata, + metadata, accountName, containerName, name: blobName, @@ -114,7 +120,9 @@ export default class PageBlobHandler extends BaseHandler contentMD5: options.blobHTTPHeaders.blobContentMD5, contentDisposition: options.blobHTTPHeaders.blobContentDisposition, cacheControl: options.blobHTTPHeaders.blobCacheControl, - blobSequenceNumber: options.blobSequenceNumber, + blobSequenceNumber: options.blobSequenceNumber + ? options.blobSequenceNumber + : 0, blobType: Models.BlobType.PageBlob, leaseStatus: Models.LeaseStatusType.Unlocked, leaseState: Models.LeaseStateType.Available, @@ -135,7 +143,8 @@ export default class PageBlobHandler extends BaseHandler await this.metadataStore.createBlob( context, blob, - options.leaseAccessConditions + options.leaseAccessConditions, + options.modifiedAccessConditions ); const response: Models.PageBlobCreateResponse = { @@ -226,7 +235,9 @@ export default class PageBlobHandler extends BaseHandler start, end, persistency, - options.leaseAccessConditions + options.leaseAccessConditions, + options.modifiedAccessConditions, + options.sequenceNumberAccessConditions ); const response: Models.PageBlobUploadPagesResponse = { @@ -295,7 +306,9 @@ export default class PageBlobHandler extends BaseHandler blob, start, end, - options.leaseAccessConditions + options.leaseAccessConditions, + options.modifiedAccessConditions, + options.sequenceNumberAccessConditions ); const response: Models.PageBlobClearPagesResponse = { @@ -329,7 +342,8 @@ export default class PageBlobHandler extends BaseHandler containerName, blobName, options.snapshot, - options.leaseAccessConditions + options.leaseAccessConditions, + options.modifiedAccessConditions ); if (blob.properties.blobType !== Models.BlobType.PageBlob) { @@ -401,7 +415,8 @@ export default class PageBlobHandler extends BaseHandler containerName, blobName, blobContentLength, - options.leaseAccessConditions + options.leaseAccessConditions, + options.modifiedAccessConditions ); const response: Models.PageBlobResizeResponse = { @@ -436,7 +451,8 @@ export default class PageBlobHandler extends BaseHandler blobName, sequenceNumberAction, options.blobSequenceNumber, - options.leaseAccessConditions + options.leaseAccessConditions, + options.modifiedAccessConditions ); const response: Models.PageBlobUpdateSequenceNumberResponse = { diff --git a/src/blob/middlewares/StrictModelMiddlewareFactory.ts b/src/blob/middlewares/StrictModelMiddlewareFactory.ts index bd205c85d..9551627bf 100644 --- a/src/blob/middlewares/StrictModelMiddlewareFactory.ts +++ b/src/blob/middlewares/StrictModelMiddlewareFactory.ts @@ -20,17 +20,6 @@ export const UnsupportedHeadersBlocker: StrictModelRequestValidator = async ( logger: ILogger ): Promise => { const UnsupportedHeaderKeys = [ - HeaderConstants.IF_MATCH, - HeaderConstants.IF_NONE_MATCH, - HeaderConstants.IF_MODIFIED_SINCE, - HeaderConstants.IF_UNMODIFIED_SINCE, - HeaderConstants.SOURCE_IF_MATCH, - HeaderConstants.SOURCE_IF_MODIFIED_SINCE, - HeaderConstants.SOURCE_IF_NONE_MATCH, - HeaderConstants.SOURCE_IF_UNMODIFIED_SINCE, - HeaderConstants.X_MS_IF_SEQUENCE_NUMBER_LE, - HeaderConstants.X_MS_IF_SEQUENCE_NUMBER_LT, - HeaderConstants.X_MS_IF_SEQUENCE_NUMBER_EQ, HeaderConstants.X_MS_BLOB_CONDITION_MAXSIZE, HeaderConstants.X_MS_BLOB_CONDITION_APPENDPOS, HeaderConstants.X_MS_CONTENT_CRC64, diff --git a/src/blob/middlewares/blobStorageContext.middleware.ts b/src/blob/middlewares/blobStorageContext.middleware.ts index 7985c99e0..e56fe2802 100644 --- a/src/blob/middlewares/blobStorageContext.middleware.ts +++ b/src/blob/middlewares/blobStorageContext.middleware.ts @@ -2,6 +2,7 @@ import { NextFunction, Request, Response } from "express"; import uuid from "uuid/v4"; import logger from "../../common/Logger"; +import { PRODUCTION_STYLE_URL_HOSTNAME } from "../../common/utils/constants"; import { checkApiVersion } from "../../common/utils/utils"; import BlobStorageContext from "../context/BlobStorageContext"; import StorageErrorFactory from "../errors/StorageErrorFactory"; @@ -52,6 +53,7 @@ export default function blobStorageContextMiddleware( ); const [account, container, blob, isSecondary] = extractStoragePartsFromPath( + req.hostname, req.path ); @@ -60,7 +62,8 @@ export default function blobStorageContextMiddleware( blobContext.blob = blob; blobContext.isSecondary = isSecondary; - // Emulator's URL pattern is like http://hostname:port/account/container + // Emulator's URL pattern is like http://hostname[:port]/account/container + // (or, alternatively, http[s]://account.localhost[:port]/container) // Create a router to exclude account name from req.path, as url path in swagger doesn't include account // Exclude account name from req.path for dispatchMiddleware blobContext.dispatchPattern = container @@ -104,6 +107,7 @@ export default function blobStorageContextMiddleware( * @returns {([string | undefined, string | undefined, string | undefined, boolean | undefined])} */ export function extractStoragePartsFromPath( + hostname: string, path: string ): [ string | undefined, @@ -123,10 +127,18 @@ export function extractStoragePartsFromPath( const parts = normalizedPath.split("/"); - account = parts[0]; - container = parts[1]; + let urlPartIndex = 0; + if (hostname.endsWith(PRODUCTION_STYLE_URL_HOSTNAME)) { + account = hostname.substring( + 0, + hostname.length - PRODUCTION_STYLE_URL_HOSTNAME.length + ); + } else { + account = parts[urlPartIndex++]; + } + container = parts[urlPartIndex++]; blob = parts - .slice(2) + .slice(urlPartIndex++) .join("/") .replace(/\\/g, "/"); // Azure Storage Server will replace "\" with "/" in the blob names diff --git a/src/blob/persistence/IBlobMetadataStore.ts b/src/blob/persistence/IBlobMetadataStore.ts index fc772ca9b..9390f550a 100644 --- a/src/blob/persistence/IBlobMetadataStore.ts +++ b/src/blob/persistence/IBlobMetadataStore.ts @@ -61,6 +61,7 @@ interface ISetContainerAccessPolicyOptions { containerAcl?: Models.SignedIdentifier[]; publicAccess?: Models.PublicAccessType; leaseAccessConditions?: Models.LeaseAccessConditions; + modifiedAccessConditions?: Models.ModifiedAccessConditions; } export type SetContainerAccessPolicyOptions = ISetContainerAccessPolicyOptions; @@ -296,7 +297,7 @@ export interface IBlobMetadataStore * @param {string} account * @param {string} container * @param {Context} context - * @param {Models.LeaseAccessConditions} [leaseAccessConditions] + * @param {Models.ContainerDeleteMethodOptionalParams} [options] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -304,18 +305,20 @@ export interface IBlobMetadataStore context: Context, account: string, container: string, - leaseAccessConditions?: Models.LeaseAccessConditions + options?: Models.ContainerDeleteMethodOptionalParams ): Promise; - /** Set container metadata. + /** + * Set container metadata. * + * @param {Context} context * @param {string} account * @param {string} container * @param {Date} lastModified * @param {string} etag - * @param {Context} context * @param {IContainerMetadata} [metadata] * @param {Models.LeaseAccessConditions} [leaseAccessConditions] + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -326,7 +329,8 @@ export interface IBlobMetadataStore lastModified: Date, etag: string, metadata?: IContainerMetadata, - leaseAccessConditions?: Models.LeaseAccessConditions + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise; /** @@ -368,7 +372,7 @@ export interface IBlobMetadataStore * * @param {string} account * @param {string} container - * @param {Models.ContainerAcquireLeaseOptionalParams} options + * @param {Models.ContainerAcquireLeaseOptionalParams} [options] * @param {Context} context * @returns {Promise} * @memberof IBlobMetadataStore @@ -377,16 +381,17 @@ export interface IBlobMetadataStore context: Context, account: string, container: string, - options: Models.ContainerAcquireLeaseOptionalParams + options?: Models.ContainerAcquireLeaseOptionalParams ): Promise; /** - * Release container lease + * Release container lease. * + * @param {Context} context * @param {string} account * @param {string} container * @param {string} leaseId - * @param {Context} context + * @param {Models.ContainerReleaseLeaseOptionalParams} [options] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -394,16 +399,18 @@ export interface IBlobMetadataStore context: Context, account: string, container: string, - leaseId: string + leaseId: string, + options?: Models.ContainerReleaseLeaseOptionalParams ): Promise; /** - * Renew container lease + * Renew container lease. * + * @param {Context} context * @param {string} account * @param {string} container * @param {string} leaseId - * @param {Context} context + * @param {Models.ContainerRenewLeaseOptionalParams} [options] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -411,16 +418,18 @@ export interface IBlobMetadataStore context: Context, account: string, container: string, - leaseId: string + leaseId: string, + options?: Models.ContainerRenewLeaseOptionalParams ): Promise; /** - * Break container lease + * Break container lease. * + * @param {Context} context * @param {string} account * @param {string} container * @param {(number | undefined)} breakPeriod - * @param {Context} context + * @param {Models.ContainerBreakLeaseOptionalParams} [options] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -428,17 +437,19 @@ export interface IBlobMetadataStore context: Context, account: string, container: string, - breakPeriod: number | undefined + breakPeriod: number | undefined, + options?: Models.ContainerBreakLeaseOptionalParams ): Promise; /** - * Change container lease + * Change container lease. * + * @param {Context} context * @param {string} account * @param {string} container * @param {string} leaseId * @param {string} proposedLeaseId - * @param {Context} context + * @param {Models.ContainerChangeLeaseOptionalParams} [options] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -447,7 +458,8 @@ export interface IBlobMetadataStore account: string, container: string, leaseId: string, - proposedLeaseId: string + proposedLeaseId: string, + options?: Models.ContainerChangeLeaseOptionalParams ): Promise; /** @@ -488,13 +500,15 @@ export interface IBlobMetadataStore * @param {Context} context * @param {BlobModel} blob * @param {Models.LeaseAccessConditions} [leaseAccessConditions] Optional. Will validate lease if provided + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof IBlobMetadataStore */ createBlob( context: Context, blob: BlobModel, - leaseAccessConditions?: Models.LeaseAccessConditions + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise; /** @@ -505,6 +519,8 @@ export interface IBlobMetadataStore * @param {string} container * @param {string} blob * @param {Models.LeaseAccessConditions} [leaseAccessConditions] Optional. Will validate lease if provided + * @param {Models.BlobMetadata} [metadata] + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -514,7 +530,8 @@ export interface IBlobMetadataStore container: string, blob: string, leaseAccessConditions?: Models.LeaseAccessConditions, - metadata?: Models.BlobMetadata + metadata?: Models.BlobMetadata, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise; /** @@ -527,6 +544,7 @@ export interface IBlobMetadataStore * @param {string} blob * @param {(string | undefined)} snapshot * @param {Models.LeaseAccessConditions} [leaseAccessConditions] Optional. Will validate lease if provided + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -536,7 +554,8 @@ export interface IBlobMetadataStore container: string, blob: string, snapshot: string | undefined, - leaseAccessConditions?: Models.LeaseAccessConditions + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise; /** @@ -548,6 +567,7 @@ export interface IBlobMetadataStore * @param {string} blob * @param {(string | undefined)} snapshot * @param {(Models.LeaseAccessConditions | undefined)} leaseAccessConditions + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -557,7 +577,8 @@ export interface IBlobMetadataStore container: string, blob: string, snapshot: string | undefined, - leaseAccessConditions: Models.LeaseAccessConditions | undefined + leaseAccessConditions: Models.LeaseAccessConditions | undefined, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise; /** @@ -588,6 +609,7 @@ export interface IBlobMetadataStore * @param {string} blob * @param {(Models.LeaseAccessConditions | undefined)} leaseAccessConditions * @param {(Models.BlobHTTPHeaders | undefined)} blobHTTPHeaders + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -597,7 +619,8 @@ export interface IBlobMetadataStore container: string, blob: string, leaseAccessConditions: Models.LeaseAccessConditions | undefined, - blobHTTPHeaders: Models.BlobHTTPHeaders | undefined + blobHTTPHeaders: Models.BlobHTTPHeaders | undefined, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise; /** @@ -609,6 +632,7 @@ export interface IBlobMetadataStore * @param {string} blob * @param {(Models.LeaseAccessConditions | undefined)} leaseAccessConditions * @param {(Models.BlobMetadata | undefined)} metadata + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -618,7 +642,8 @@ export interface IBlobMetadataStore container: string, blob: string, leaseAccessConditions: Models.LeaseAccessConditions | undefined, - metadata: Models.BlobMetadata | undefined + metadata: Models.BlobMetadata | undefined, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise; /** @@ -630,6 +655,7 @@ export interface IBlobMetadataStore * @param {string} blob * @param {number} duration * @param {string} [proposedLeaseId] + * @param {Models.BlobAcquireLeaseOptionalParams} [options] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -639,7 +665,8 @@ export interface IBlobMetadataStore container: string, blob: string, duration: number, - proposedLeaseId?: string + proposedLeaseId?: string, + options?: Models.BlobAcquireLeaseOptionalParams ): Promise; /** @@ -650,6 +677,7 @@ export interface IBlobMetadataStore * @param {string} container * @param {string} blob * @param {string} leaseId + * @param {Models.BlobReleaseLeaseOptionalParams} [options] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -658,7 +686,8 @@ export interface IBlobMetadataStore account: string, container: string, blob: string, - leaseId: string + leaseId: string, + options?: Models.BlobReleaseLeaseOptionalParams ): Promise; /** @@ -669,6 +698,7 @@ export interface IBlobMetadataStore * @param {string} container * @param {string} blob * @param {string} leaseId + * @param {Models.BlobRenewLeaseOptionalParams} [options] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -677,7 +707,8 @@ export interface IBlobMetadataStore account: string, container: string, blob: string, - leaseId: string + leaseId: string, + options?: Models.BlobRenewLeaseOptionalParams ): Promise; /** @@ -689,6 +720,7 @@ export interface IBlobMetadataStore * @param {string} blob * @param {string} leaseId * @param {string} proposedLeaseId + * @param {Models.BlobChangeLeaseOptionalParams} [option] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -698,17 +730,19 @@ export interface IBlobMetadataStore container: string, blob: string, leaseId: string, - proposedLeaseId: string + proposedLeaseId: string, + option?: Models.BlobChangeLeaseOptionalParams ): Promise; /** - * Break blob lease + * Break blob lease. * * @param {Context} context * @param {string} account * @param {string} container * @param {string} blob * @param {number} [breakPeriod] + * @param {Models.BlobBreakLeaseOptionalParams} [option] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -717,7 +751,8 @@ export interface IBlobMetadataStore account: string, container: string, blob: string, - breakPeriod?: number + breakPeriod?: number, + option?: Models.BlobBreakLeaseOptionalParams ): Promise; /** @@ -769,7 +804,7 @@ export interface IBlobMetadataStore * @param {string} copySource * @param {(Models.BlobMetadata | undefined)} metadata * @param {(Models.AccessTier | undefined)} tier - * @param {(Models.LeaseAccessConditions | undefined)} leaseAccessConditions + * @param {Models.BlobStartCopyFromURLOptionalParams} [leaseAccessConditions] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -780,7 +815,7 @@ export interface IBlobMetadataStore copySource: string, metadata: Models.BlobMetadata | undefined, tier: Models.AccessTier | undefined, - leaseAccessConditions: Models.LeaseAccessConditions | undefined + leaseAccessConditions?: Models.BlobStartCopyFromURLOptionalParams ): Promise; /** @@ -822,10 +857,11 @@ export interface IBlobMetadataStore /** * Commit block list for a blob. * + * @param {Context} context * @param {BlobModel} blob * @param {{ blockName: string; blockCommitType: string }[]} blockList - * @param {(Models.LeaseAccessConditions | undefined)} leaseAccessConditions - * @param {Context} context + * @param {Models.LeaseAccessConditions} [leaseAccessConditions] + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -833,7 +869,8 @@ export interface IBlobMetadataStore context: Context, blob: BlobModel, blockList: { blockName: string; blockCommitType: string }[], - leaseAccessConditions: Models.LeaseAccessConditions | undefined + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise; /** @@ -867,11 +904,14 @@ export interface IBlobMetadataStore /** * Upload new pages for page blob. * + * @param {Context} context * @param {BlobModel} blob * @param {number} start * @param {number} end * @param {IExtentChunk} persistency - * @param {Context} [context] + * @param {Models.LeaseAccessConditions} [leaseAccessConditions] + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] + * @param {Models.SequenceNumberAccessConditions} [sequenceNumberAccessConditions] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -881,16 +921,21 @@ export interface IBlobMetadataStore start: number, end: number, persistency: IExtentChunk, - leaseAccessConditions?: Models.LeaseAccessConditions + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions, + sequenceNumberAccessConditions?: Models.SequenceNumberAccessConditions ): Promise; /** * Clear range for a page blob. * + * @param {Context} context * @param {BlobModel} blob * @param {number} start * @param {number} end - * @param {Context} [context] + * @param {Models.LeaseAccessConditions} [leaseAccessConditions] + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] + * @param {Models.SequenceNumberAccessConditions} [sequenceNumberAccessConditions] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -899,7 +944,9 @@ export interface IBlobMetadataStore blob: BlobModel, start: number, end: number, - leaseAccessConditions?: Models.LeaseAccessConditions + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions, + sequenceNumberAccessConditions?: Models.SequenceNumberAccessConditions ): Promise; /** @@ -910,6 +957,8 @@ export interface IBlobMetadataStore * @param {string} container * @param {string} blob * @param {string} [snapshot] + * @param {Models.LeaseAccessConditions} [leaseAccessConditions] + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -919,17 +968,20 @@ export interface IBlobMetadataStore container: string, blob: string, snapshot?: string, - leaseAccessConditions?: Models.LeaseAccessConditions + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise; /** * Resize a page blob. * + * @param {Context} context * @param {string} account * @param {string} container * @param {string} blob * @param {number} blobContentLength - * @param {Context} context + * @param {Models.LeaseAccessConditions} [leaseAccessConditions] + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -939,18 +991,21 @@ export interface IBlobMetadataStore container: string, blob: string, blobContentLength: number, - leaseAccessConditions?: Models.LeaseAccessConditions + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise; /** * Update the sequence number of a page blob. * + * @param {Context} context * @param {string} account * @param {string} container * @param {string} blob * @param {Models.SequenceNumberActionType} sequenceNumberAction * @param {(number | undefined)} blobSequenceNumber - * @param {Context} context + * @param {Models.LeaseAccessConditions} [leaseAccessConditions] + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof IBlobMetadataStore */ @@ -961,7 +1016,8 @@ export interface IBlobMetadataStore blob: string, sequenceNumberAction: Models.SequenceNumberActionType, blobSequenceNumber: number | undefined, - leaseAccessConditions?: Models.LeaseAccessConditions + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise; /** diff --git a/src/blob/persistence/LokiBlobMetadataStore.ts b/src/blob/persistence/LokiBlobMetadataStore.ts index 9f4d4a78b..3a55f3832 100644 --- a/src/blob/persistence/LokiBlobMetadataStore.ts +++ b/src/blob/persistence/LokiBlobMetadataStore.ts @@ -7,9 +7,13 @@ import { convertDateTimeStringMsTo7Digital, rimrafAsync } from "../../common/utils/utils"; +import { validateReadConditions } from "../conditions/ReadConditionalHeadersValidator"; +import { + validateSequenceNumberWriteConditions, + validateWriteConditions +} from "../conditions/WriteConditionalHeadersValidator"; import StorageErrorFactory from "../errors/StorageErrorFactory"; import * as Models from "../generated/artifacts/models"; -import { LeaseStatusType } from "../generated/artifacts/models"; import Context from "../generated/Context"; import PageBlobRangesManager from "../handlers/PageBlobRangesManager"; import BlobLeaseAdapter from "../lease/BlobLeaseAdapter"; @@ -428,7 +432,7 @@ export default class LokiBlobMetadataStore * @param {string} account * @param {string} container * @param {Context} context - * @param {Models.LeaseAccessConditions} [leaseAccessConditions] + * @param {Models.ContainerDeleteMethodOptionalParams} [options] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -436,16 +440,23 @@ export default class LokiBlobMetadataStore context: Context, account: string, container: string, - leaseAccessConditions?: Models.LeaseAccessConditions + options: Models.ContainerDeleteMethodOptionalParams = {} ): Promise { const coll = this.db.getCollection(this.CONTAINERS_COLLECTION); const doc = await this.getContainerWithLeaseUpdated( account, container, - context + context, + false ); - new ContainerDeleteLeaseValidator(leaseAccessConditions).validate( + validateWriteConditions(context, options.modifiedAccessConditions, doc); + + if (!doc) { + throw StorageErrorFactory.getContainerNotFound(context.contextId); + } + + new ContainerDeleteLeaseValidator(options.leaseAccessConditions).validate( new ContainerLeaseAdapter(doc), context ); @@ -468,13 +479,14 @@ export default class LokiBlobMetadataStore /** * Set container metadata. * + * @param {Context} context * @param {string} account * @param {string} container * @param {Date} lastModified * @param {string} etag - * @param {Context} context * @param {IContainerMetadata} [metadata] * @param {Models.LeaseAccessConditions} [leaseAccessConditions] + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -485,15 +497,23 @@ export default class LokiBlobMetadataStore lastModified: Date, etag: string, metadata?: IContainerMetadata, - leaseAccessConditions?: Models.LeaseAccessConditions + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { const coll = this.db.getCollection(this.CONTAINERS_COLLECTION); const doc = await this.getContainerWithLeaseUpdated( account, container, - context + context, + false ); + validateWriteConditions(context, modifiedAccessConditions, doc); + + if (!doc) { + throw StorageErrorFactory.getContainerNotFound(context.contextId); + } + new ContainerReadLeaseValidator(leaseAccessConditions).validate( new ContainerLeaseAdapter(doc), context @@ -561,9 +581,16 @@ export default class LokiBlobMetadataStore const doc = await this.getContainerWithLeaseUpdated( account, container, - context + context, + false ); + validateWriteConditions(context, setAclModel.modifiedAccessConditions, doc); + + if (!doc) { + throw StorageErrorFactory.getContainerNotFound(context.contextId); + } + new ContainerReadLeaseValidator(setAclModel.leaseAccessConditions).validate( new ContainerLeaseAdapter(doc), context @@ -594,7 +621,13 @@ export default class LokiBlobMetadataStore options: Models.ContainerAcquireLeaseOptionalParams ): Promise { const coll = this.db.getCollection(this.CONTAINERS_COLLECTION); - const doc = await this.getContainer(account, container, context); + const doc = await this.getContainer(account, container, context, false); + + validateWriteConditions(context, options.modifiedAccessConditions, doc); + + if (!doc) { + throw StorageErrorFactory.getContainerNotFound(context.contextId); + } LeaseFactory.createLeaseState(new ContainerLeaseAdapter(doc), context) .acquire(options.duration!, options.proposedLeaseId) @@ -608,10 +641,11 @@ export default class LokiBlobMetadataStore /** * Release container lease. * + * @param {Context} context * @param {string} account * @param {string} container * @param {string} leaseId - * @param {Context} context + * @param {Models.ContainerReleaseLeaseOptionalParams} [options={}] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -619,10 +653,17 @@ export default class LokiBlobMetadataStore context: Context, account: string, container: string, - leaseId: string + leaseId: string, + options: Models.ContainerReleaseLeaseOptionalParams = {} ): Promise { const coll = this.db.getCollection(this.CONTAINERS_COLLECTION); - const doc = await this.getContainer(account, container, context); + const doc = await this.getContainer(account, container, context, false); + + validateWriteConditions(context, options.modifiedAccessConditions, doc); + + if (!doc) { + throw StorageErrorFactory.getContainerNotFound(context.contextId); + } LeaseFactory.createLeaseState(new ContainerLeaseAdapter(doc), context) .release(leaseId) @@ -636,10 +677,11 @@ export default class LokiBlobMetadataStore /** * Renew container lease. * + * @param {Context} context * @param {string} account * @param {string} container * @param {string} leaseId - * @param {Context} context + * @param {Models.ContainerRenewLeaseOptionalParams} [options={}] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -647,10 +689,17 @@ export default class LokiBlobMetadataStore context: Context, account: string, container: string, - leaseId: string + leaseId: string, + options: Models.ContainerRenewLeaseOptionalParams = {} ): Promise { const coll = this.db.getCollection(this.CONTAINERS_COLLECTION); - const doc = await this.getContainer(account, container, context); + const doc = await this.getContainer(account, container, context, false); + + validateWriteConditions(context, options.modifiedAccessConditions, doc); + + if (!doc) { + throw StorageErrorFactory.getContainerNotFound(context.contextId); + } LeaseFactory.createLeaseState(new ContainerLeaseAdapter(doc), context) .renew(leaseId) @@ -664,10 +713,11 @@ export default class LokiBlobMetadataStore /** * Break container lease. * + * @param {Context} context * @param {string} account * @param {string} container * @param {(number | undefined)} breakPeriod - * @param {Context} context + * @param {Models.ContainerBreakLeaseOptionalParams} [options={}] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -675,10 +725,17 @@ export default class LokiBlobMetadataStore context: Context, account: string, container: string, - breakPeriod: number | undefined + breakPeriod: number | undefined, + options: Models.ContainerBreakLeaseOptionalParams = {} ): Promise { const coll = this.db.getCollection(this.CONTAINERS_COLLECTION); - const doc = await this.getContainer(account, container, context); + const doc = await this.getContainer(account, container, context, false); + + validateWriteConditions(context, options.modifiedAccessConditions, doc); + + if (!doc) { + throw StorageErrorFactory.getContainerNotFound(context.contextId); + } LeaseFactory.createLeaseState(new ContainerLeaseAdapter(doc), context) .break(breakPeriod) @@ -700,11 +757,12 @@ export default class LokiBlobMetadataStore /** * Change container lease. * + * @param {Context} context * @param {string} account * @param {string} container * @param {string} leaseId * @param {string} proposedLeaseId - * @param {Context} context + * @param {Models.ContainerChangeLeaseOptionalParams} [options={}] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -713,10 +771,17 @@ export default class LokiBlobMetadataStore account: string, container: string, leaseId: string, - proposedLeaseId: string + proposedLeaseId: string, + options: Models.ContainerChangeLeaseOptionalParams = {} ): Promise { const coll = this.db.getCollection(this.CONTAINERS_COLLECTION); - const doc = await this.getContainer(account, container, context); + const doc = await this.getContainer(account, container, context, false); + + validateWriteConditions(context, options.modifiedAccessConditions, doc); + + if (!doc) { + throw StorageErrorFactory.getContainerNotFound(context.contextId); + } LeaseFactory.createLeaseState(new ContainerLeaseAdapter(doc), context) .change(leaseId, proposedLeaseId) @@ -880,13 +945,15 @@ export default class LokiBlobMetadataStore * @param {Context} context * @param {BlobModel} blob * @param {Models.LeaseAccessConditions} [leaseAccessConditions] + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof LokiBlobMetadataStore */ public async createBlob( context: Context, blob: BlobModel, - leaseAccessConditions?: Models.LeaseAccessConditions + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { await this.checkContainerExist( context, @@ -901,6 +968,17 @@ export default class LokiBlobMetadataStore snapshot: blob.snapshot }); + validateWriteConditions(context, modifiedAccessConditions, blobDoc); + + // Create if not exists + if ( + modifiedAccessConditions && + modifiedAccessConditions.ifNoneMatch === "*" && + blobDoc + ) { + throw StorageErrorFactory.getBlobAlreadyExists(context.contextId); + } + if (blobDoc) { LeaseFactory.createLeaseState( new BlobLeaseAdapter(blobDoc), @@ -936,7 +1014,8 @@ export default class LokiBlobMetadataStore container: string, blob: string, leaseAccessConditions?: Models.LeaseAccessConditions, - metadata?: Models.BlobMetadata + metadata?: Models.BlobMetadata, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { const coll = this.db.getCollection(this.BLOBS_COLLECTION); const doc = await this.getBlobWithLeaseUpdated( @@ -944,9 +1023,17 @@ export default class LokiBlobMetadataStore container, blob, undefined, - context + context, + false, + true ); + validateReadConditions(context, modifiedAccessConditions, doc); + + if (!doc) { + throw StorageErrorFactory.getBlobNotFound(context.contextId); + } + new BlobReadLeaseValidator(leaseAccessConditions).validate( new BlobLeaseAdapter(doc), context @@ -1003,6 +1090,7 @@ export default class LokiBlobMetadataStore * @param {string} blob * @param {string} [snapshot=""] * @param {Models.LeaseAccessConditions} [leaseAccessConditions] + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -1012,16 +1100,25 @@ export default class LokiBlobMetadataStore container: string, blob: string, snapshot: string = "", - leaseAccessConditions?: Models.LeaseAccessConditions + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { const doc = await this.getBlobWithLeaseUpdated( account, container, blob, snapshot, - context + context, + false, + true ); + validateReadConditions(context, modifiedAccessConditions, doc); + + if (!doc) { + throw StorageErrorFactory.getBlobNotFound(context.contextId); + } + new BlobReadLeaseValidator(leaseAccessConditions).validate( new BlobLeaseAdapter(doc), context @@ -1068,13 +1165,15 @@ export default class LokiBlobMetadataStore } /** - * Get blob properties + * Get blob properties. * * @param {Context} context * @param {string} account * @param {string} container * @param {string} blob * @param {string} [snapshot=""] + * @param {(Models.LeaseAccessConditions | undefined)} leaseAccessConditions + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -1084,16 +1183,26 @@ export default class LokiBlobMetadataStore container: string, blob: string, snapshot: string = "", - leaseAccessConditions: Models.LeaseAccessConditions | undefined + leaseAccessConditions: Models.LeaseAccessConditions | undefined, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { const doc = await this.getBlobWithLeaseUpdated( account, container, blob, snapshot, - context + context, + false, + true ); + validateReadConditions(context, modifiedAccessConditions, doc); + + // When block blob don't have commited block, should return 404 + if (!doc) { + throw StorageErrorFactory.getBlobNotFound(context.contextId); + } + new BlobReadLeaseValidator(leaseAccessConditions).validate( new BlobLeaseAdapter(doc), context @@ -1128,8 +1237,16 @@ export default class LokiBlobMetadataStore container, blob, options.snapshot, - context + context, + false ); + + validateWriteConditions(context, options.modifiedAccessConditions, doc); + + if (!doc) { + throw StorageErrorFactory.getBlobNotFound(context.contextId); + } + const againstBaseBlob = doc.snapshot === ""; // Check bad requests @@ -1207,9 +1324,9 @@ export default class LokiBlobMetadataStore * @param {string} account * @param {string} container * @param {string} blob - * @param {(string | undefined)} snapshot * @param {(Models.LeaseAccessConditions | undefined)} leaseAccessConditions * @param {(Models.BlobHTTPHeaders | undefined)} blobHTTPHeaders + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -1219,7 +1336,8 @@ export default class LokiBlobMetadataStore container: string, blob: string, leaseAccessConditions: Models.LeaseAccessConditions | undefined, - blobHTTPHeaders: Models.BlobHTTPHeaders | undefined + blobHTTPHeaders: Models.BlobHTTPHeaders | undefined, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { const coll = this.db.getCollection(this.BLOBS_COLLECTION); const doc = await this.getBlobWithLeaseUpdated( @@ -1227,9 +1345,17 @@ export default class LokiBlobMetadataStore container, blob, undefined, - context + context, + false, + true ); + validateWriteConditions(context, modifiedAccessConditions, doc); + + if (!doc) { + throw StorageErrorFactory.getBlobNotFound(context.contextId); + } + const lease = new BlobLeaseAdapter(doc); new BlobWriteLeaseValidator(leaseAccessConditions).validate(lease, context); @@ -1248,12 +1374,10 @@ export default class LokiBlobMetadataStore blobProps.contentEncoding = blobHeaders.blobContentEncoding; blobProps.contentLanguage = blobHeaders.blobContentLanguage; blobProps.contentDisposition = blobHeaders.blobContentDisposition; - blobProps.lastModified = context.startTime - ? context.startTime - : new Date(); } doc.properties = blobProps; doc.properties.etag = newEtag(); + blobProps.lastModified = context.startTime ? context.startTime : new Date(); new BlobWriteLeaseSyncer(doc).sync(lease); @@ -1270,6 +1394,7 @@ export default class LokiBlobMetadataStore * @param {string} blob * @param {(Models.LeaseAccessConditions | undefined)} leaseAccessConditions * @param {(Models.BlobMetadata | undefined)} metadata + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -1279,7 +1404,8 @@ export default class LokiBlobMetadataStore container: string, blob: string, leaseAccessConditions: Models.LeaseAccessConditions | undefined, - metadata: Models.BlobMetadata | undefined + metadata: Models.BlobMetadata | undefined, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { const coll = this.db.getCollection(this.BLOBS_COLLECTION); const doc = await this.getBlobWithLeaseUpdated( @@ -1287,14 +1413,23 @@ export default class LokiBlobMetadataStore container, blob, undefined, - context + context, + false, + true ); + validateWriteConditions(context, modifiedAccessConditions, doc); + + if (!doc) { + throw StorageErrorFactory.getBlobNotFound(context.contextId); + } + const lease = new BlobLeaseAdapter(doc); new BlobWriteLeaseValidator(leaseAccessConditions).validate(lease, context); new BlobWriteLeaseSyncer(doc).sync(lease); doc.metadata = metadata; doc.properties.etag = newEtag(); + doc.properties.lastModified = context.startTime || new Date(); coll.update(doc); return doc.properties; } @@ -1308,6 +1443,7 @@ export default class LokiBlobMetadataStore * @param {string} blob * @param {number} duration * @param {string} [proposedLeaseId] + * @param {Models.BlobAcquireLeaseOptionalParams} [options={}] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -1317,7 +1453,8 @@ export default class LokiBlobMetadataStore container: string, blob: string, duration: number, - proposedLeaseId?: string + proposedLeaseId?: string, + options: Models.BlobAcquireLeaseOptionalParams = {} ): Promise { const coll = this.db.getCollection(this.BLOBS_COLLECTION); const doc = await this.getBlobWithLeaseUpdated( @@ -1325,11 +1462,19 @@ export default class LokiBlobMetadataStore container, blob, undefined, - context - ); + context, + false + ); // This may return an uncommitted blob, or undefined for an unexist blob + + validateWriteConditions(context, options.modifiedAccessConditions, doc); + + // Azure Storage allows lease for a uncommitted blob + if (!doc) { + throw StorageErrorFactory.getBlobNotFound(context.contextId); + } if (doc.snapshot !== "") { - throw StorageErrorFactory.getBlobSnapshotsPresent(context.contextId!); + throw StorageErrorFactory.getBlobSnapshotsPresent(context.contextId); } LeaseFactory.createLeaseState(new BlobLeaseAdapter(doc), context) @@ -1349,6 +1494,7 @@ export default class LokiBlobMetadataStore * @param {string} container * @param {string} blob * @param {string} leaseId + * @param {Models.BlobReleaseLeaseOptionalParams} [options={}] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -1357,7 +1503,8 @@ export default class LokiBlobMetadataStore account: string, container: string, blob: string, - leaseId: string + leaseId: string, + options: Models.BlobReleaseLeaseOptionalParams = {} ): Promise { const coll = this.db.getCollection(this.BLOBS_COLLECTION); const doc = await this.getBlobWithLeaseUpdated( @@ -1365,8 +1512,16 @@ export default class LokiBlobMetadataStore container, blob, undefined, - context - ); + context, + false + ); // This may return an uncommitted blob, or undefined for an unexist blob + + validateWriteConditions(context, options.modifiedAccessConditions, doc); + + // Azure Storage allows lease for a uncommitted blob + if (!doc) { + throw StorageErrorFactory.getBlobNotFound(context.contextId); + } if (doc.snapshot !== "") { throw StorageErrorFactory.getBlobSnapshotsPresent(context.contextId!); @@ -1389,6 +1544,7 @@ export default class LokiBlobMetadataStore * @param {string} container * @param {string} blob * @param {string} leaseId + * @param {Models.BlobRenewLeaseOptionalParams} [options={}] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -1397,7 +1553,8 @@ export default class LokiBlobMetadataStore account: string, container: string, blob: string, - leaseId: string + leaseId: string, + options: Models.BlobRenewLeaseOptionalParams = {} ): Promise { const coll = this.db.getCollection(this.BLOBS_COLLECTION); const doc = await this.getBlobWithLeaseUpdated( @@ -1405,8 +1562,16 @@ export default class LokiBlobMetadataStore container, blob, undefined, - context - ); + context, + false + ); // This may return an uncommitted blob, or undefined for an unexist blob + + validateWriteConditions(context, options.modifiedAccessConditions, doc); + + // Azure Storage allows lease for a uncommitted blob + if (!doc) { + throw StorageErrorFactory.getBlobNotFound(context.contextId); + } if (doc.snapshot !== "") { throw StorageErrorFactory.getBlobSnapshotsPresent(context.contextId!); @@ -1430,6 +1595,7 @@ export default class LokiBlobMetadataStore * @param {string} blob * @param {string} leaseId * @param {string} proposedLeaseId + * @param {Models.BlobChangeLeaseOptionalParams} [option={}] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -1439,7 +1605,8 @@ export default class LokiBlobMetadataStore container: string, blob: string, leaseId: string, - proposedLeaseId: string + proposedLeaseId: string, + options: Models.BlobChangeLeaseOptionalParams = {} ): Promise { const coll = this.db.getCollection(this.BLOBS_COLLECTION); const doc = await this.getBlobWithLeaseUpdated( @@ -1447,8 +1614,16 @@ export default class LokiBlobMetadataStore container, blob, undefined, - context - ); + context, + false + ); // This may return an uncommitted blob, or undefined for an unexist blob + + validateWriteConditions(context, options.modifiedAccessConditions, doc); + + // Azure Storage allows lease for a uncommitted blob + if (!doc) { + throw StorageErrorFactory.getBlobNotFound(context.contextId); + } if (doc.snapshot !== "") { throw StorageErrorFactory.getBlobSnapshotsPresent(context.contextId!); @@ -1471,6 +1646,7 @@ export default class LokiBlobMetadataStore * @param {string} container * @param {string} blob * @param {(number | undefined)} breakPeriod + * @param {Models.BlobBreakLeaseOptionalParams} [options={}] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -1479,7 +1655,8 @@ export default class LokiBlobMetadataStore account: string, container: string, blob: string, - breakPeriod: number | undefined + breakPeriod: number | undefined, + options: Models.BlobBreakLeaseOptionalParams = {} ): Promise { const coll = this.db.getCollection(this.BLOBS_COLLECTION); const doc = await this.getBlobWithLeaseUpdated( @@ -1487,8 +1664,16 @@ export default class LokiBlobMetadataStore container, blob, undefined, - context - ); + context, + false + ); // This may return an uncommitted blob, or undefined for an unexist blob + + validateWriteConditions(context, options.modifiedAccessConditions, doc); + + // Azure Storage allows lease for a uncommitted blob + if (!doc) { + throw StorageErrorFactory.getBlobNotFound(context.contextId); + } if (doc.snapshot !== "") { throw StorageErrorFactory.getBlobSnapshotsPresent(context.contextId!); @@ -1579,14 +1764,15 @@ export default class LokiBlobMetadataStore } /** - * start copy from Url + * Start copy from Url. * * @param {Context} context * @param {BlobId} source * @param {BlobId} destination + * @param {string} copySource * @param {(Models.BlobMetadata | undefined)} metadata * @param {(Models.AccessTier | undefined)} tier - * @param {(Models.LeaseAccessConditions | undefined)} leaseAccessConditions + * @param {Models.BlobStartCopyFromURLOptionalParams} [leaseAccessConditions] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -1597,7 +1783,7 @@ export default class LokiBlobMetadataStore copySource: string, metadata: Models.BlobMetadata | undefined, tier: Models.AccessTier | undefined, - leaseAccessConditions: Models.LeaseAccessConditions | undefined + options: Models.BlobStartCopyFromURLOptionalParams = {} ): Promise { const coll = this.db.getCollection(this.BLOBS_COLLECTION); const sourceBlob = await this.getBlobWithLeaseUpdated( @@ -1605,7 +1791,24 @@ export default class LokiBlobMetadataStore source.container, source.blob, source.snapshot, - context + context, + true, + true + ); + + 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( @@ -1617,8 +1820,14 @@ export default class LokiBlobMetadataStore false ); + validateWriteConditions( + context, + options.modifiedAccessConditions, + destBlob + ); + if (destBlob) { - new BlobWriteLeaseValidator(leaseAccessConditions).validate( + new BlobWriteLeaseValidator(options.leaseAccessConditions).validate( new BlobLeaseAdapter(destBlob), context ); @@ -1755,7 +1964,9 @@ export default class LokiBlobMetadataStore container, blob, undefined, - context + context, + true, + true ); let responseCode: 200 | 202 = 200; @@ -1831,6 +2042,8 @@ export default class LokiBlobMetadataStore name: block.blobName }); + let blobExist = false; + if (!blobDoc) { const etag = newEtag(); const newBlob = { @@ -1853,9 +2066,28 @@ export default class LokiBlobMetadataStore LeaseFactory.createLeaseState(new BlobLeaseAdapter(blobDoc), context) .validate(new BlobWriteLeaseValidator(leaseAccessConditions)) .sync(new BlobWriteLeaseSyncer(blobDoc)); + blobExist = true; } const coll = this.db.getCollection(this.BLOCKS_COLLECTION); + + // If the new block ID does not have same length with before uncommited block ID, return failure. + if (blobExist) { + const existBlockDoc = coll.findOne({ + accountName: block.accountName, + containerName: block.containerName, + blobName: block.blobName + }); + if (existBlockDoc) { + if ( + Buffer.from(existBlockDoc.name, "base64").length !== + Buffer.from(block.name, "base64").length + ) { + throw StorageErrorFactory.getInvalidBlobOrBlock(context.contextId); + } + } + } + const blockDoc = coll.findOne({ accountName: block.accountName, containerName: block.containerName, @@ -1875,10 +2107,11 @@ export default class LokiBlobMetadataStore /** * Commit block list for a blob. * + * @param {Context} context * @param {BlobModel} blob * @param {{ blockName: string; blockCommitType: string }[]} blockList - * @param {(Models.LeaseAccessConditions | undefined)} leaseAccessConditions - * @param {Context} context + * @param {Models.LeaseAccessConditions} [leaseAccessConditions] + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -1886,7 +2119,8 @@ export default class LokiBlobMetadataStore context: Context, blob: BlobModel, blockList: { blockName: string; blockCommitType: string }[], - leaseAccessConditions: Models.LeaseAccessConditions | undefined + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { const coll = this.db.getCollection(this.BLOBS_COLLECTION); const doc = await this.getBlobWithLeaseUpdated( @@ -1900,6 +2134,18 @@ export default class LokiBlobMetadataStore false ); + validateWriteConditions(context, modifiedAccessConditions, doc); + + // Create if not exists + if ( + modifiedAccessConditions && + modifiedAccessConditions.ifNoneMatch === "*" && + doc && + doc.isCommitted + ) { + throw StorageErrorFactory.getBlobAlreadyExists(context.contextId); + } + let lease: ILease | undefined; if (doc) { lease = new BlobLeaseAdapter(doc); @@ -2092,11 +2338,14 @@ export default class LokiBlobMetadataStore /** * Upload new pages for page blob. * + * @param {Context} context * @param {BlobModel} blob * @param {number} start * @param {number} end * @param {IExtentChunk} persistency - * @param {Context} [context] + * @param {Models.LeaseAccessConditions} [leaseAccessConditions] + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] + * @param {Models.SequenceNumberAccessConditions} [sequenceNumberAccessConditions] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -2106,7 +2355,9 @@ export default class LokiBlobMetadataStore start: number, end: number, persistency: IExtentChunk, - leaseAccessConditions?: Models.LeaseAccessConditions + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions, + sequenceNumberAccessConditions?: Models.SequenceNumberAccessConditions ): Promise { const coll = this.db.getCollection(this.BLOBS_COLLECTION); const doc = await this.getBlobWithLeaseUpdated( @@ -2114,9 +2365,23 @@ export default class LokiBlobMetadataStore blob.containerName, blob.name, blob.snapshot, - context! + context!, + false, + true + ); + + validateWriteConditions(context, modifiedAccessConditions, doc); + + validateSequenceNumberWriteConditions( + context, + sequenceNumberAccessConditions, + doc ); + if (!doc) { + throw StorageErrorFactory.getBlobNotFound(context.contextId); + } + const lease = new BlobLeaseAdapter(doc); new BlobWriteLeaseValidator(leaseAccessConditions).validate(lease, context); @@ -2130,6 +2395,7 @@ export default class LokiBlobMetadataStore new BlobWriteLeaseSyncer(doc).sync(lease); doc.properties.etag = newEtag(); + doc.properties.lastModified = context.startTime || new Date(); coll.update(doc); @@ -2139,10 +2405,13 @@ export default class LokiBlobMetadataStore /** * Clear range for a page blob. * + * @param {Context} context * @param {BlobModel} blob * @param {number} start * @param {number} end - * @param {Context} [context] + * @param {Models.LeaseAccessConditions} [leaseAccessConditions] + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] + * @param {Models.SequenceNumberAccessConditions} [sequenceNumberAccessConditions] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -2151,7 +2420,9 @@ export default class LokiBlobMetadataStore blob: BlobModel, start: number, end: number, - leaseAccessConditions?: Models.LeaseAccessConditions + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions, + sequenceNumberAccessConditions?: Models.SequenceNumberAccessConditions ): Promise { const coll = this.db.getCollection(this.BLOBS_COLLECTION); const doc = await this.getBlobWithLeaseUpdated( @@ -2159,9 +2430,23 @@ export default class LokiBlobMetadataStore blob.containerName, blob.name, blob.snapshot, - context! + context!, + false, + true + ); + + validateWriteConditions(context, modifiedAccessConditions, doc); + + validateSequenceNumberWriteConditions( + context, + sequenceNumberAccessConditions, + doc ); + if (!doc) { + throw StorageErrorFactory.getBlobNotFound(context.contextId); + } + const lease = new BlobLeaseAdapter(doc); new BlobWriteLeaseValidator(leaseAccessConditions).validate(lease, context); @@ -2175,6 +2460,8 @@ export default class LokiBlobMetadataStore new BlobWriteLeaseSyncer(doc).sync(lease); doc.properties.etag = newEtag(); + doc.properties.lastModified = context.startTime || new Date(); + coll.update(doc); return doc.properties; @@ -2188,6 +2475,8 @@ export default class LokiBlobMetadataStore * @param {string} container * @param {string} blob * @param {string} [snapshot] + * @param {Models.LeaseAccessConditions} [leaseAccessConditions] + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -2197,16 +2486,25 @@ export default class LokiBlobMetadataStore container: string, blob: string, snapshot?: string, - leaseAccessConditions?: Models.LeaseAccessConditions + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { const doc = await this.getBlobWithLeaseUpdated( account, container, blob, snapshot, - context + context, + false, + true ); + validateReadConditions(context, modifiedAccessConditions, doc); + + if (!doc) { + throw StorageErrorFactory.getBlobNotFound(context.contextId); + } + new BlobReadLeaseValidator(leaseAccessConditions).validate( new BlobLeaseAdapter(doc), context @@ -2221,11 +2519,13 @@ export default class LokiBlobMetadataStore /** * Resize a page blob. * + * @param {Context} context * @param {string} account * @param {string} container * @param {string} blob * @param {number} blobContentLength - * @param {Context} context + * @param {Models.LeaseAccessConditions} [leaseAccessConditions] + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -2235,7 +2535,8 @@ export default class LokiBlobMetadataStore container: string, blob: string, blobContentLength: number, - leaseAccessConditions?: Models.LeaseAccessConditions + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { const coll = this.db.getCollection(this.BLOBS_COLLECTION); const doc = await this.getBlobWithLeaseUpdated( @@ -2243,13 +2544,20 @@ export default class LokiBlobMetadataStore container, blob, undefined, - context + context, + false, + true ); - const requestId = context ? context.contextId : undefined; + + validateWriteConditions(context, modifiedAccessConditions, doc); + + if (!doc) { + throw StorageErrorFactory.getBlobNotFound(context.contextId); + } if (doc.properties.blobType !== Models.BlobType.PageBlob) { throw StorageErrorFactory.getInvalidOperation( - requestId, + context.contextId, "Resize could only be against a page blob." ); } @@ -2268,7 +2576,7 @@ export default class LokiBlobMetadataStore } doc.properties.contentLength = blobContentLength; - doc.properties.lastModified = context.startTime!; + doc.properties.lastModified = context.startTime || new Date(); doc.properties.etag = newEtag(); new BlobWriteLeaseSyncer(doc).sync(lease); @@ -2280,12 +2588,14 @@ export default class LokiBlobMetadataStore /** * Update the sequence number of a page blob. * + * @param {Context} context * @param {string} account * @param {string} container * @param {string} blob * @param {Models.SequenceNumberActionType} sequenceNumberAction * @param {(number | undefined)} blobSequenceNumber - * @param {Context} context + * @param {Models.LeaseAccessConditions} [leaseAccessConditions] + * @param {Models.ModifiedAccessConditions} [modifiedAccessConditions] * @returns {Promise} * @memberof LokiBlobMetadataStore */ @@ -2296,7 +2606,8 @@ export default class LokiBlobMetadataStore blob: string, sequenceNumberAction: Models.SequenceNumberActionType, blobSequenceNumber: number | undefined, - leaseAccessConditions?: Models.LeaseAccessConditions + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { const coll = this.db.getCollection(this.BLOBS_COLLECTION); const doc = await this.getBlobWithLeaseUpdated( @@ -2304,9 +2615,17 @@ export default class LokiBlobMetadataStore container, blob, undefined, - context + context, + false, + true ); + validateWriteConditions(context, modifiedAccessConditions, doc); + + if (!doc) { + throw StorageErrorFactory.getBlobNotFound(context.contextId); + } + if (doc.properties.blobType !== Models.BlobType.PageBlob) { throw StorageErrorFactory.getInvalidOperation( context.contextId!, @@ -2360,6 +2679,7 @@ export default class LokiBlobMetadataStore } doc.properties.etag = newEtag(); + doc.properties.lastModified = context.startTime!; new BlobWriteLeaseSyncer(doc).sync(lease); coll.update(doc); @@ -2450,15 +2770,63 @@ export default class LokiBlobMetadataStore * @returns {Promise} * @memberof LokiBlobMetadataStore */ + + /** + * Get a container document from container collection. + * Updated lease related properties according to current time. + * Will throw ContainerNotFound storage error if container doesn't exist. + * + * @private + * @param {string} account + * @param {string} container + * @param {Context} context + * @returns {Promise} + * @memberof LokiBlobMetadataStore + */ private async getContainerWithLeaseUpdated( account: string, container: string, - context: Context - ): Promise { + context: Context, + forceExist?: true + ): Promise; + + /** + * Get a container document from container collection. + * Updated lease related properties according to current time. + * Will NOT throw ContainerNotFound storage error if container doesn't exist. + * + * @private + * @param {string} account + * @param {string} container + * @param {Context} context + * @param {false} forceExist + * @returns {(Promise)} + * @memberof LokiBlobMetadataStore + */ + private async getContainerWithLeaseUpdated( + account: string, + container: string, + context: Context, + forceExist: false + ): Promise; + + private async getContainerWithLeaseUpdated( + account: string, + container: string, + context: Context, + forceExist?: boolean + ): Promise { const coll = this.db.getCollection(this.CONTAINERS_COLLECTION); const doc = coll.findOne({ accountName: account, name: container }); + + if (forceExist === undefined || forceExist === true) { + if (!doc) { + throw StorageErrorFactory.getContainerNotFound(context.contextId); + } + } + if (!doc) { - throw StorageErrorFactory.getContainerNotFound(context.contextId); + return undefined; } LeaseFactory.createLeaseState(new ContainerLeaseAdapter(doc), context).sync( @@ -2469,72 +2837,121 @@ export default class LokiBlobMetadataStore } /** - * Get a container document from collections. + * Get a container document from Loki collections. + * Will throw ContainerNotFound error when container doesn't exist. * + * @private * @param {string} account * @param {string} container * @param {Context} context + * @param {true} [forceExist] * @returns {Promise} * @memberof LokiBlobMetadataStore */ private async getContainer( account: string, container: string, - context: Context - ): Promise { + context: Context, + forceExist?: true + ): Promise; + + /** + * Get a container document from Loki collections. + * Will NOT throw ContainerNotFound error when container doesn't exist. + * + * @private + * @param {string} account + * @param {string} container + * @param {Context} context + * @param {false} forceExist + * @returns {Promise} + * @memberof LokiBlobMetadataStore + */ + private async getContainer( + account: string, + container: string, + context: Context, + forceExist: false + ): Promise; + + private async getContainer( + account: string, + container: string, + context: Context, + forceExist?: boolean + ): Promise { const coll = this.db.getCollection(this.CONTAINERS_COLLECTION); const doc = coll.findOne({ accountName: account, name: container }); + if (!doc) { - const requestId = context ? context.contextId : undefined; - throw StorageErrorFactory.getContainerNotFound(requestId); + if (forceExist) { + throw StorageErrorFactory.getContainerNotFound(context.contextId); + } else { + return undefined; + } } return doc; } /** - * Get a blob doc from collections. + * Get a blob document model from Loki collection. + * Will throw BlobNotFound storage error if blob doesn't exist. * * @private * @param {string} account * @param {string} container * @param {string} blob - * @param {string} snapshot + * @param {(string | undefined)} snapshot * @param {Context} context + * @param {undefined} [forceExist] + * @param {boolean} [forceCommitted] If true, will take uncommitted blob as a non-exist blob and throw exception. * @returns {Promise} * @memberof LokiBlobMetadataStore */ - private async getBlobWithLeaseUpdated( - account: string, - container: string, - blob: string, - snapshot: string | undefined, - context: Context - ): Promise; private async getBlobWithLeaseUpdated( account: string, container: string, blob: string, snapshot: string | undefined, context: Context, - // tslint:disable-next-line:unified-signatures - forceExist: true + forceExist?: true, + forceCommitted?: boolean ): Promise; + + /** + * Get a blob document model from Loki collection. + * Will NOT throw BlobNotFound storage error if blob doesn't exist. + * + * @private + * @param {string} account + * @param {string} container + * @param {string} blob + * @param {(string | undefined)} snapshot + * @param {Context} context + * @param {false} forceExist + * @param {boolean} [forceCommitted] If true, will take uncommitted blob as a non-exist blob and return undefined. + * @returns {(Promise)} + * @memberof LokiBlobMetadataStore + */ private async getBlobWithLeaseUpdated( account: string, container: string, blob: string, snapshot: string | undefined, context: Context, - forceExist: false + forceExist: false, + forceCommitted?: boolean ): Promise; + private async getBlobWithLeaseUpdated( account: string, container: string, blob: string, snapshot: string = "", context: Context, - forceExist?: boolean + forceExist?: boolean, + forceCommitted?: boolean ): Promise { await this.checkContainerExist(context, account, container); @@ -2546,14 +2963,27 @@ export default class LokiBlobMetadataStore snapshot }); + // Force exist if parameter forceExist is undefined or true if (forceExist === undefined || forceExist === true) { - if (!doc) { - throw StorageErrorFactory.getBlobNotFound(context.contextId); + 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) { - return undefined; } if (doc.properties) { @@ -2571,7 +3001,7 @@ export default class LokiBlobMetadataStore leaseBreakTime: undefined, leaseDurationType: undefined, leaseState: Models.LeaseStateType.Available, // TODO: Lease state & status should be undefined for snapshots - leaseStatus: LeaseStatusType.Unlocked // TODO: Lease state & status should be undefined for snapshots + leaseStatus: Models.LeaseStatusType.Unlocked // TODO: Lease state & status should be undefined for snapshots }); } else { LeaseFactory.createLeaseState(new BlobLeaseAdapter(doc), context).sync( diff --git a/src/blob/persistence/SqlBlobMetadataStore.ts b/src/blob/persistence/SqlBlobMetadataStore.ts index 3e8e72ca9..aa74eca72 100644 --- a/src/blob/persistence/SqlBlobMetadataStore.ts +++ b/src/blob/persistence/SqlBlobMetadataStore.ts @@ -15,9 +15,10 @@ import { DEFAULT_SQL_COLLATE } from "../../common/utils/constants"; import { convertDateTimeStringMsTo7Digital } 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 { BlobType, LeaseAccessConditions } from "../generated/artifacts/models"; import Context from "../generated/Context"; import BlobLeaseAdapter from "../lease/BlobLeaseAdapter"; import BlobLeaseSyncer from "../lease/BlobLeaseSyncer"; @@ -591,12 +592,18 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { context: Context, account: string, container: string, - leaseAccessConditions?: Models.LeaseAccessConditions + options: Models.ContainerDeleteMethodOptionalParams = {} ): Promise { await this.sequelize.transaction(async t => { /* Transaction starts */ const findResult = await ContainersModel.findOne({ - attributes: ["lease"], + attributes: [ + "accountName", + "containerName", + "etag", + "lastModified", + "lease" + ], where: { accountName: account, containerName: container @@ -604,6 +611,12 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); + validateWriteConditions( + context, + options.modifiedAccessConditions, + findResult ? this.convertDbModelToContainerModel(findResult) : undefined + ); + if (findResult === null || findResult === undefined) { throw StorageErrorFactory.getContainerNotFound(context.contextId); } @@ -611,7 +624,9 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { LeaseFactory.createLeaseState( this.convertDbModelToLease(findResult), context - ).validate(new ContainerDeleteLeaseValidator(leaseAccessConditions)); + ).validate( + new ContainerDeleteLeaseValidator(options.leaseAccessConditions) + ); await ContainersModel.destroy({ where: { @@ -659,12 +674,19 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { lastModified: Date, etag: string, metadata?: IContainerMetadata, - leaseAccessConditions?: LeaseAccessConditions + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { return this.sequelize.transaction(async t => { /* Transaction starts */ const findResult = await ContainersModel.findOne({ - attributes: ["lease"], + attributes: [ + "accountName", + "containerName", + "etag", + "lastModified", + "lease" + ], where: { accountName: account, containerName: container @@ -672,6 +694,12 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); + validateWriteConditions( + context, + modifiedAccessConditions, + findResult ? this.convertDbModelToContainerModel(findResult) : undefined + ); + if (findResult === null || findResult === undefined) { throw StorageErrorFactory.getContainerNotFound(context.contextId); } @@ -739,7 +767,13 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { ): Promise { await this.sequelize.transaction(async t => { const findResult = await ContainersModel.findOne({ - attributes: ["lease"], + attributes: [ + "accountName", + "containerName", + "etag", + "lastModified", + "lease" + ], where: { accountName: account, containerName: container @@ -747,6 +781,12 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); + validateWriteConditions( + context, + setAclModel.modifiedAccessConditions, + findResult ? this.convertDbModelToContainerModel(findResult) : undefined + ); + if (findResult === null || findResult === undefined) { throw StorageErrorFactory.getContainerNotFound(context.contextId); } @@ -796,6 +836,12 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); + validateWriteConditions( + context, + options.modifiedAccessConditions, + findResult ? this.convertDbModelToContainerModel(findResult) : undefined + ); + if (findResult === null || findResult === undefined) { throw StorageErrorFactory.getContainerNotFound(context.contextId); } @@ -831,7 +877,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { context: Context, account: string, container: string, - leaseId: string + leaseId: string, + options: Models.ContainerReleaseLeaseOptionalParams = {} ): Promise { return this.sequelize.transaction(async t => { /* Transaction starts */ @@ -843,6 +890,12 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); + validateWriteConditions( + context, + options.modifiedAccessConditions, + findResult ? this.convertDbModelToContainerModel(findResult) : undefined + ); + if (findResult === null || findResult === undefined) { throw StorageErrorFactory.getContainerNotFound(context.contextId); } @@ -876,7 +929,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { context: Context, account: string, container: string, - leaseId: string + leaseId: string, + options: Models.ContainerRenewLeaseOptionalParams = {} ): Promise { return this.sequelize.transaction(async t => { /* Transaction starts */ @@ -889,6 +943,12 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); + validateWriteConditions( + context, + options.modifiedAccessConditions, + findResult ? this.convertDbModelToContainerModel(findResult) : undefined + ); + if (findResult === null || findResult === undefined) { throw StorageErrorFactory.getContainerNotFound(context.contextId); } @@ -925,7 +985,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { context: Context, account: string, container: string, - breakPeriod: number | undefined + breakPeriod: number | undefined, + options: Models.ContainerBreakLeaseOptionalParams = {} ): Promise { return this.sequelize.transaction(async t => { const findResult = await ContainersModel.findOne({ @@ -936,6 +997,12 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); + validateWriteConditions( + context, + options.modifiedAccessConditions, + findResult ? this.convertDbModelToContainerModel(findResult) : undefined + ); + if (findResult === null || findResult === undefined) { throw StorageErrorFactory.getContainerNotFound(context.contextId); } @@ -982,7 +1049,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { account: string, container: string, leaseId: string, - proposedLeaseId: string + proposedLeaseId: string, + options: Models.ContainerChangeLeaseOptionalParams = {} ): Promise { return this.sequelize.transaction(async t => { const findResult = await ContainersModel.findOne({ @@ -993,6 +1061,12 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); + validateWriteConditions( + context, + options.modifiedAccessConditions, + findResult ? this.convertDbModelToContainerModel(findResult) : undefined + ); + if (findResult === null || findResult === undefined) { throw StorageErrorFactory.getContainerNotFound(context.contextId); } @@ -1043,7 +1117,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { public async createBlob( context: Context, blob: BlobModel, - leaseAccessConditions?: Models.LeaseAccessConditions + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { return this.sequelize.transaction(async t => { const containerFindResult = await ContainersModel.findOne({ @@ -1070,6 +1145,23 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); + validateWriteConditions( + context, + modifiedAccessConditions, + blobFindResult + ? this.convertDbModelToBlobModel(blobFindResult) + : undefined + ); + + // Create if not exists + if ( + modifiedAccessConditions && + modifiedAccessConditions.ifNoneMatch === "*" && + blobFindResult + ) { + throw StorageErrorFactory.getBlobAlreadyExists(context.contextId); + } + if (blobFindResult) { const blobModel: BlobModel = this.convertDbModelToBlobModel( blobFindResult @@ -1100,7 +1192,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { container: string, blob: string, snapshot: string = "", - leaseAccessConditions?: Models.LeaseAccessConditions + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { return this.sequelize.transaction(async t => { const containerFindResult = await ContainersModel.findOne({ @@ -1127,6 +1220,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); + validateReadConditions( + context, + modifiedAccessConditions, + blobFindResult + ? this.convertDbModelToBlobModel(blobFindResult) + : undefined + ); + if (blobFindResult === null || blobFindResult === undefined) { throw StorageErrorFactory.getBlobNotFound(context.contextId); } @@ -1286,8 +1387,7 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { containerName: block.containerName, blobName: block.blobName, snapshot: "", - deleting: 0, - isCommitted: true + deleting: 0 }, transaction: t }); @@ -1297,10 +1397,34 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { blobFindResult ); - LeaseFactory.createLeaseState( - new BlobLeaseAdapter(blobModel), - context - ).validate(new BlobWriteLeaseValidator(leaseAccessConditions)); + if (blobModel.isCommitted === true) { + LeaseFactory.createLeaseState( + new BlobLeaseAdapter(blobModel), + context + ).validate(new BlobWriteLeaseValidator(leaseAccessConditions)); + } + + // If the new block ID does not have same length with before uncommited block ID, return failure. + 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(existBlock, "blockName", true), + "base64" + ).length !== Buffer.from(block.name, "base64").length + ) { + throw StorageErrorFactory.getInvalidBlobOrBlock(context.contextId); + } } else { const newBlob = { deleted: false, @@ -1407,7 +1531,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { context: Context, blob: BlobModel, blockList: { blockName: string; blockCommitType: string }[], - leaseAccessConditions: Models.LeaseAccessConditions | undefined + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { await this.sequelize.transaction(async t => { const containerFindResult = await ContainersModel.findOne({ @@ -1444,6 +1569,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); + validateWriteConditions( + context, + modifiedAccessConditions, + blobFindResult + ? this.convertDbModelToBlobModel(blobFindResult) // TODO: Reduce duplicated convert + : undefined + ); + let creationTime = blob.properties.creationTime || context.startTime; if (blobFindResult !== null && blobFindResult !== undefined) { @@ -1451,6 +1584,16 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { blobFindResult ); + // Create if not exists + if ( + modifiedAccessConditions && + modifiedAccessConditions.ifNoneMatch === "*" && + blobModel && + blobModel.isCommitted + ) { + throw StorageErrorFactory.getBlobAlreadyExists(context.contextId); + } + creationTime = blobModel.properties.creationTime || creationTime; LeaseFactory.createLeaseState( @@ -1530,7 +1673,7 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { .reduce((total, val) => { return total + val; }, 0), - blobType: BlobType.BlockBlob + blobType: Models.BlobType.BlockBlob } }; @@ -1570,7 +1713,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { container: string, blob: string, snapshot: string = "", - leaseAccessConditions?: Models.LeaseAccessConditions + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { return this.sequelize.transaction(async t => { const containerFindResult = await ContainersModel.findOne({ @@ -1597,6 +1741,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); + validateReadConditions( + context, + modifiedAccessConditions, + blobFindResult + ? this.convertDbModelToBlobModel(blobFindResult) + : undefined + ); + if (blobFindResult === null || blobFindResult === undefined) { throw StorageErrorFactory.getBlobNotFound(context.contextId); } @@ -1605,6 +1757,10 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { blobFindResult ); + if (!blobModel.isCommitted) { + throw StorageErrorFactory.getBlobNotFound(context.contextId); + } + return LeaseFactory.createLeaseState( new BlobLeaseAdapter(blobModel), context @@ -1624,7 +1780,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { container: string, blob: string, leaseAccessConditions?: Models.LeaseAccessConditions, - metadata?: Models.BlobMetadata + metadata?: Models.BlobMetadata, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { return this.sequelize.transaction(async t => { const containerFindResult = await ContainersModel.findOne({ @@ -1652,6 +1809,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); + validateReadConditions( + context, + modifiedAccessConditions, + blobFindResult + ? this.convertDbModelToBlobModel(blobFindResult) // TODO: Reduce double convert + : undefined + ); + if (blobFindResult === null || blobFindResult === undefined) { throw StorageErrorFactory.getBlobNotFound(context.contextId); } @@ -1719,11 +1884,19 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { blobName: blob, snapshot: options.snapshot === undefined ? "" : options.snapshot, deleting: 0, - isCommitted: true + isCommitted: true // TODO: Support deleting uncommitted block blob }, transaction: t }); + validateWriteConditions( + context, + options.modifiedAccessConditions, + blobFindResult + ? this.convertDbModelToBlobModel(blobFindResult) // TODO: Reduce double convert + : undefined + ); + if (blobFindResult === null || blobFindResult === undefined) { throw StorageErrorFactory.getBlobNotFound(context.contextId); } @@ -1873,7 +2046,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { container: string, blob: string, leaseAccessConditions: Models.LeaseAccessConditions | undefined, - blobHTTPHeaders: Models.BlobHTTPHeaders | undefined + blobHTTPHeaders: Models.BlobHTTPHeaders | undefined, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { return this.sequelize.transaction(async t => { const containerFindResult = await ContainersModel.findOne({ @@ -1901,6 +2075,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); + validateWriteConditions( + context, + modifiedAccessConditions, + blobFindResult + ? this.convertDbModelToBlobModel(blobFindResult) // TODO: Reduce double convert + : undefined + ); + if (blobFindResult === null || blobFindResult === undefined) { throw StorageErrorFactory.getBlobNotFound(context.contextId); } @@ -1923,12 +2105,12 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { blobHTTPHeaders.blobContentLanguage; blobModel.properties.contentDisposition = blobHTTPHeaders.blobContentDisposition; - blobModel.properties.lastModified = context.startTime - ? context.startTime - : new Date(); } blobModel.properties.etag = newEtag(); + blobModel.properties.lastModified = context.startTime + ? context.startTime + : new Date(); await BlobsModel.update(this.convertBlobModelToDbModel(blobModel), { where: { @@ -1951,7 +2133,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { container: string, blob: string, leaseAccessConditions: Models.LeaseAccessConditions | undefined, - metadata: Models.BlobMetadata | undefined + metadata: Models.BlobMetadata | undefined, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { return this.sequelize.transaction(async t => { const containerFindResult = await ContainersModel.findOne({ @@ -1979,6 +2162,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); + validateWriteConditions( + context, + modifiedAccessConditions, + blobFindResult + ? this.convertDbModelToBlobModel(blobFindResult) // TODO: Reduce double convert + : undefined + ); + if (blobFindResult === null || blobFindResult === undefined) { throw StorageErrorFactory.getBlobNotFound(context.contextId); } @@ -2029,7 +2220,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { container: string, blob: string, duration: number, - proposedLeaseId?: string + proposedLeaseId?: string, + options: Models.BlobAcquireLeaseOptionalParams = {} ): Promise { return this.sequelize.transaction(async t => { const containerFindResult = await ContainersModel.findOne({ @@ -2057,6 +2249,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); + validateWriteConditions( + context, + options.modifiedAccessConditions, + blobFindResult + ? this.convertDbModelToBlobModel(blobFindResult) // TODO: Reduce double convert + : undefined + ); + if (blobFindResult === null || blobFindResult === undefined) { throw StorageErrorFactory.getBlobNotFound(context.contextId); } @@ -2087,7 +2287,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { account: string, container: string, blob: string, - leaseId: string + leaseId: string, + options: Models.BlobReleaseLeaseOptionalParams = {} ): Promise { return this.sequelize.transaction(async t => { const containerFindResult = await ContainersModel.findOne({ @@ -2115,6 +2316,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); + validateWriteConditions( + context, + options.modifiedAccessConditions, + blobFindResult + ? this.convertDbModelToBlobModel(blobFindResult) // TODO: Reduce double convert + : undefined + ); + if (blobFindResult === null || blobFindResult === undefined) { throw StorageErrorFactory.getBlobNotFound(context.contextId); } @@ -2145,7 +2354,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { account: string, container: string, blob: string, - leaseId: string + leaseId: string, + options: Models.BlobRenewLeaseOptionalParams = {} ): Promise { return this.sequelize.transaction(async t => { const containerFindResult = await ContainersModel.findOne({ @@ -2173,6 +2383,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); + validateWriteConditions( + context, + options.modifiedAccessConditions, + blobFindResult + ? this.convertDbModelToBlobModel(blobFindResult) // TODO: Reduce double convert + : undefined + ); + if (blobFindResult === null || blobFindResult === undefined) { throw StorageErrorFactory.getBlobNotFound(context.contextId); } @@ -2204,7 +2422,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { container: string, blob: string, leaseId: string, - proposedLeaseId: string + proposedLeaseId: string, + options: Models.BlobChangeLeaseOptionalParams = {} ): Promise { return this.sequelize.transaction(async t => { const containerFindResult = await ContainersModel.findOne({ @@ -2232,6 +2451,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); + validateWriteConditions( + context, + options.modifiedAccessConditions, + blobFindResult + ? this.convertDbModelToBlobModel(blobFindResult) // TODO: Reduce double convert + : undefined + ); + if (blobFindResult === null || blobFindResult === undefined) { throw StorageErrorFactory.getBlobNotFound(context.contextId); } @@ -2262,7 +2489,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { account: string, container: string, blob: string, - breakPeriod: number | undefined + breakPeriod: number | undefined, + options: Models.BlobBreakLeaseOptionalParams = {} ): Promise { return this.sequelize.transaction(async t => { const containerFindResult = await ContainersModel.findOne({ @@ -2290,6 +2518,14 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); + validateWriteConditions( + context, + options.modifiedAccessConditions, + blobFindResult + ? this.convertDbModelToBlobModel(blobFindResult) // TODO: Reduce double convert + : undefined + ); + if (blobFindResult === null || blobFindResult === undefined) { throw StorageErrorFactory.getBlobNotFound(context.contextId); } @@ -2515,7 +2751,9 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { blob: BlobModel, start: number, end: number, - persistencycontext: import("./IBlobMetadataStore").IExtentChunk + persistency: IExtentChunk, + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { throw new Error("Method not implemented."); } @@ -2524,7 +2762,9 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { context: Context, blob: BlobModel, start: number, - end: number + end: number, + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { throw new Error("Method not implemented."); } @@ -2534,7 +2774,9 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { account: string, container: string, blob: string, - snapshot?: string | undefined + snapshot?: string | undefined, + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { throw new Error("Method not implemented."); } @@ -2544,7 +2786,9 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { account: string, container: string, blob: string, - blobContentLength: number + blobContentLength: number, + leaseAccessConditions?: Models.LeaseAccessConditions, + modifiedAccessConditions?: Models.ModifiedAccessConditions ): Promise { throw new Error("Method not implemented."); } diff --git a/src/blob/utils/constants.ts b/src/blob/utils/constants.ts index 910c8c96e..d6e21206f 100644 --- a/src/blob/utils/constants.ts +++ b/src/blob/utils/constants.ts @@ -1,7 +1,7 @@ import { StoreDestinationArray } from "../../common/persistence/IExtentStore"; import * as Models from "../generated/artifacts/models"; -export const VERSION = "3.5.0"; +export const VERSION = "3.6.0"; export const BLOB_API_VERSION = "2019-07-07"; export const DEFAULT_BLOB_SERVER_HOST_NAME = "127.0.0.1"; // Change to 0.0.0.0 when needs external access export const DEFAULT_LIST_BLOBS_MAX_RESULTS = 5000; @@ -52,6 +52,8 @@ export const HeaderConstants = { X_MS_IF_SEQUENCE_NUMBER_EQ: "x-ms-if-sequence-number-eq", X_MS_BLOB_CONDITION_MAXSIZE: "x-ms-blob-condition-maxsize", X_MS_BLOB_CONDITION_APPENDPOS: "x-ms-blob-condition-appendpos", + X_MS_SEQUENCE_NUMBER_ACTION: "x-ms-sequence-number-action", + X_MS_BLOB_SEQUENCE_NUMBER: "x-ms-blob-sequence-number", X_MS_CONTENT_CRC64: "x-ms-content-crc64", X_MS_RANGE_GET_CONTENT_CRC64: "x-ms-range-get-content-crc64", X_MS_ENCRYPTION_KEY: "x-ms-encryption-key", diff --git a/src/common/utils/constants.ts b/src/common/utils/constants.ts index e2f3cb1d9..153558f33 100644 --- a/src/common/utils/constants.ts +++ b/src/common/utils/constants.ts @@ -24,3 +24,9 @@ export const DEFAULT_SQL_OPTIONS = { timezone: "+00:00" } }; + +// In some scenarios, users want to test with production-style URLs like +// http[s]://devstoreaccount1.localhost[:port]/container/path/blob.dat +// (as opposed to default emulator style http[s]://hostname[:port]/devstoreaccount1/container/path/blob.dat +// When URL's hostname ends with .localhost, we assume user wants to use production-style URL format. +export const PRODUCTION_STYLE_URL_HOSTNAME = ".localhost"; diff --git a/src/common/utils/utils.ts b/src/common/utils/utils.ts index 757b9e93f..81afd470f 100644 --- a/src/common/utils/utils.ts +++ b/src/common/utils/utils.ts @@ -31,3 +31,30 @@ export function convertDateTimeStringMsTo7Digital( ): string { return dateTimeString.replace("Z", "0000Z"); } + +export function convertRawHeadersToMetadata( + rawHeaders: string[] = [] +): { [propertyName: string]: string } | undefined { + const metadataPrefix = "x-ms-meta-"; + const res: { [propertyName: string]: string } = {}; + let isEmpty = true; + + for (let i = 0; i < rawHeaders.length; i = i + 2) { + const header = rawHeaders[i]; + if ( + header.startsWith(metadataPrefix) && + header.length > metadataPrefix.length + ) { + const key = header.substr(metadataPrefix.length); + let value = rawHeaders[i + 1] || ""; + if (res[key] !== undefined) { + value = `${res[key]},${value}`; + } + res[key] = value; + isEmpty = false; + continue; + } + } + + return isEmpty ? undefined : res; +} diff --git a/src/queue/QueueRequestListenerFactory.ts b/src/queue/QueueRequestListenerFactory.ts index 4ee386ed2..377bd5c55 100644 --- a/src/queue/QueueRequestListenerFactory.ts +++ b/src/queue/QueueRequestListenerFactory.ts @@ -21,6 +21,7 @@ import PreflightMiddlewareFactory from "./middlewares/PreflightMiddlewareFactory import queueStorageContextMiddleware from "./middlewares/queueStorageContext.middleware"; import { IQueueMetadataStore } from "./persistence/IQueueMetadataStore"; import { DEFAULT_QUEUE_CONTEXT_PATH } from "./utils/constants"; +import QueueTokenAuthenticator from "./authentication/QueueTokenAuthenticator"; /** * Default RequestListenerFactory based on express framework. @@ -103,7 +104,8 @@ export default class QueueRequestListenerFactory this.accountDataStore, this.metadataStore, logger - ) + ), + new QueueTokenAuthenticator(this.accountDataStore, logger) ]) ); diff --git a/src/queue/authentication/QueueTokenAuthenticator.ts b/src/queue/authentication/QueueTokenAuthenticator.ts new file mode 100644 index 000000000..a9a45dded --- /dev/null +++ b/src/queue/authentication/QueueTokenAuthenticator.ts @@ -0,0 +1,68 @@ +import IAccountDataStore from "../../common/IAccountDataStore"; +import ILogger from "../../common/ILogger"; +import QueueStorageContext from "../context/QueueStorageContext"; +import StorageErrorFactory from "../errors/StorageErrorFactory"; +import Context from "../generated/Context"; +import IRequest from "../generated/IRequest"; +import { HeaderConstants } from "../utils/constants"; +import IAuthenticator from "./IAuthenticator"; + +export default class QueueTokenAuthenticator implements IAuthenticator { + public constructor( + private readonly dataStore: IAccountDataStore, + private readonly logger: ILogger + ) {} + + public async validate( + req: IRequest, + context: Context + ): Promise { + const queueContext = new QueueStorageContext(context); + const account = queueContext.account!; + + this.logger.info( + `QueueTokenAuthenticator:validate() Start validation against token authentication.`, + queueContext.contextID + ); + + // TODO: Make following async + const accountProperties = this.dataStore.getAccount(account); + if (accountProperties === undefined) { + this.logger.error( + `QueueTokenAuthenticator:validate() Invalid storage account ${account}.`, + queueContext.contextID + ); + throw StorageErrorFactory.getInvalidOperation( + queueContext.contextID!, + "Invalid storage account." + ); + } + + const authHeaderValue = req.getHeader(HeaderConstants.AUTHORIZATION); + if (authHeaderValue === undefined) { + this.logger.info( + // tslint:disable-next-line:max-line-length + `QueueTokenAuthenticator:validate() Request doesn't include valid authentication header. Skip token authentication.`, + queueContext.contextID + ); + return undefined; + } else { + const hasBearerToken = authHeaderValue.startsWith("Bearer"); + + if (hasBearerToken) { + this.logger.info( + // tslint:disable-next-line:max-line-length + `QueueTokenAuthenticator:validate() Request includes Bearer token.`, + queueContext.contextID + ); + } else { + this.logger.info( + // tslint:disable-next-line:max-line-length + `QueueTokenAuthenticator:validate() Request does not include Bearer token. Skip token authentication.`, + queueContext.contextID + ); + } + return hasBearerToken; + } + } +} diff --git a/src/queue/errors/StorageError.ts b/src/queue/errors/StorageError.ts index 40cd94030..222469971 100644 --- a/src/queue/errors/StorageError.ts +++ b/src/queue/errors/StorageError.ts @@ -10,6 +10,10 @@ import { jsonToXML } from "../generated/utils/xml"; * @extends {MiddlewareError} */ export default class StorageError extends MiddlewareError { + public readonly storageErrorCode: string; + public readonly storageErrorMessage: string; + public readonly storageRequestID: string; + /** * Creates an instance of StorageError. * @@ -56,5 +60,8 @@ export default class StorageError extends MiddlewareError { ); this.name = "StorageError"; + this.storageErrorCode = storageErrorCode; + this.storageErrorMessage = storageErrorMessage; + this.storageRequestID = storageRequestID; } } diff --git a/src/queue/generated/ExpressRequestAdapter.ts b/src/queue/generated/ExpressRequestAdapter.ts index dced6a1ec..c37abcb31 100644 --- a/src/queue/generated/ExpressRequestAdapter.ts +++ b/src/queue/generated/ExpressRequestAdapter.ts @@ -43,6 +43,10 @@ export default class ExpressRequestAdapter implements IRequest { return this.req.headers; } + public getRawHeaders(): string[] { + return this.req.rawHeaders; + } + public getQuery(key: string): string | undefined { return this.req.query[key]; } diff --git a/src/queue/generated/IRequest.ts b/src/queue/generated/IRequest.ts index 0fb3060ea..df39dd0ad 100644 --- a/src/queue/generated/IRequest.ts +++ b/src/queue/generated/IRequest.ts @@ -19,6 +19,7 @@ export default interface IRequest { getBody(): string | undefined; getHeader(field: string): string | undefined; getHeaders(): { [header: string]: string | string[] | undefined }; + getRawHeaders(): string[]; getQuery(key: string): string | undefined; getProtocol(): string; } diff --git a/src/queue/middlewares/queueStorageContext.middleware.ts b/src/queue/middlewares/queueStorageContext.middleware.ts index 501d44120..bca2b7f95 100644 --- a/src/queue/middlewares/queueStorageContext.middleware.ts +++ b/src/queue/middlewares/queueStorageContext.middleware.ts @@ -2,6 +2,7 @@ import { NextFunction, Request, Response } from "express"; import uuid from "uuid/v4"; import logger from "../../common/Logger"; +import { PRODUCTION_STYLE_URL_HOSTNAME } from "../../common/utils/constants"; import { checkApiVersion } from "../../common/utils/utils"; import QueueStorageContext from "../context/QueueStorageContext"; import StorageErrorFactory from "../errors/StorageErrorFactory"; @@ -62,7 +63,7 @@ export default function queueStorageContextMiddleware( message, messageId, isSecondary - ] = extractStoragePartsFromPath(req.path); + ] = extractStoragePartsFromPath(req.hostname, req.path); queueContext.account = account; queueContext.queue = queue; @@ -70,7 +71,8 @@ export default function queueStorageContextMiddleware( queueContext.messageId = messageId; queueContext.isSecondary = isSecondary; - // Emulator's URL pattern is like http://hostname:port/account/queue/messages + // Emulator's URL pattern is like http://hostname[:port]/account/queue/messages + // (or, alternatively, http[s]://account.localhost[:port]/queue/messages) // Create a router to exclude account name from req.path, as url path in swagger doesn't include account // Exclude account name from req.path for dispatchMiddleware queueContext.dispatchPattern = @@ -149,6 +151,7 @@ export default function queueStorageContextMiddleware( * @returns {([string | undefined, string | undefined, string | undefined, boolean | undefined])} */ export function extractStoragePartsFromPath( + hostname: string, path: string ): [ string | undefined, @@ -170,11 +173,19 @@ export function extractStoragePartsFromPath( const parts = normalizedPath.split("/"); - account = parts[0]; - queue = parts[1]; + let urlPartIndex = 0; + if (hostname.endsWith(PRODUCTION_STYLE_URL_HOSTNAME)) { + account = hostname.substring( + 0, + hostname.length - PRODUCTION_STYLE_URL_HOSTNAME.length + ); + } else { + account = parts[urlPartIndex++]; + } + queue = parts[urlPartIndex++]; // For delete and update, it is messages/messageid?popreceipt=string-value - message = parts[2]; - messageId = parts[3]; + message = parts[urlPartIndex++]; + messageId = parts[urlPartIndex++]; if (account.endsWith(SECONDARY_SUFFIX)) { account = account.substr(0, account.length - SECONDARY_SUFFIX.length); diff --git a/src/queue/utils/constants.ts b/src/queue/utils/constants.ts index 2c96236e8..24501f5ad 100644 --- a/src/queue/utils/constants.ts +++ b/src/queue/utils/constants.ts @@ -1,6 +1,6 @@ import { StoreDestinationArray } from "../../common/persistence/IExtentStore"; -export const VERSION = "3.5.0"; +export const VERSION = "3.6.0"; export const QUEUE_API_VERSION = "2019-07-07"; export const DEFAULT_QUEUE_SERVER_HOST_NAME = "127.0.0.1"; // Change to 0.0.0.0 when needs external access export const DEFAULT_QUEUE_LISTENING_PORT = 10001; diff --git a/tests/blob/apis/blob.test.ts b/tests/blob/apis/blob.test.ts index 0016b33a7..989ad0542 100644 --- a/tests/blob/apis/blob.test.ts +++ b/tests/blob/apis/blob.test.ts @@ -80,6 +80,127 @@ describe("BlobAPIs", () => { ); }); + it("download should work with conditional headers @loki @sql", async () => { + const properties = await blobURL.getProperties(Aborter.none); + const result = await blobURL.download(Aborter.none, 0, undefined, { + blobAccessConditions: { + modifiedAccessConditions: { + ifMatch: properties.eTag, + ifNoneMatch: "invalidetag", + ifModifiedSince: new Date("2018/01/01"), + ifUnmodifiedSince: new Date("2188/01/01") + } + } + }); + assert.deepStrictEqual(await bodyToString(result, content.length), content); + assert.equal(result.contentRange, undefined); + assert.equal( + result._response.request.headers.get("x-ms-client-request-id"), + result.clientRequestId + ); + }); + + it("download should work with ifMatch value * @loki @sql", async () => { + const result = await blobURL.download(Aborter.none, 0, undefined, { + blobAccessConditions: { + modifiedAccessConditions: { + ifMatch: "*,abc", + ifNoneMatch: "invalidetag", + ifModifiedSince: new Date("2018/01/01"), + ifUnmodifiedSince: new Date("2188/01/01") + } + } + }); + assert.deepStrictEqual(await bodyToString(result, content.length), content); + assert.equal(result.contentRange, undefined); + assert.equal( + result._response.request.headers.get("x-ms-client-request-id"), + result.clientRequestId + ); + }); + + it("download should not work with invalid conditional header ifMatch @loki @sql", async () => { + const properties = await blobURL.getProperties(Aborter.none); + try { + await blobURL.download(Aborter.none, 0, undefined, { + blobAccessConditions: { + modifiedAccessConditions: { + ifMatch: properties.eTag + "invalid" + } + } + }); + } catch (error) { + assert.deepStrictEqual(error.statusCode, 412); + return; + } + assert.fail(); + }); + + it("download should not work with conditional header ifNoneMatch @loki @sql", async () => { + const properties = await blobURL.getProperties(Aborter.none); + try { + await blobURL.download(Aborter.none, 0, undefined, { + blobAccessConditions: { + modifiedAccessConditions: { + ifNoneMatch: properties.eTag + } + } + }); + } catch (error) { + assert.deepStrictEqual(error.statusCode, 304); + return; + } + assert.fail(); + }); + + it("download should not work with conditional header ifNoneMatch * @loki @sql", async () => { + try { + await blobURL.download(Aborter.none, 0, undefined, { + blobAccessConditions: { + modifiedAccessConditions: { + ifNoneMatch: "*" + } + } + }); + } catch (error) { + assert.deepStrictEqual(error.statusCode, 400); + return; + } + assert.fail(); + }); + + it("download should not work with conditional header ifModifiedSince @loki @sql", async () => { + try { + await blobURL.download(Aborter.none, 0, undefined, { + blobAccessConditions: { + modifiedAccessConditions: { + ifModifiedSince: new Date("2120/01/01") + } + } + }); + } catch (error) { + assert.deepStrictEqual(error.statusCode, 304); + return; + } + assert.fail(); + }); + + it("download should not work with conditional header ifUnmodifiedSince @loki @sql", async () => { + try { + await blobURL.download(Aborter.none, 0, undefined, { + blobAccessConditions: { + modifiedAccessConditions: { + ifUnmodifiedSince: new Date("2018/01/01") + } + } + }); + } catch (error) { + assert.deepStrictEqual(error.statusCode, 412); + return; + } + assert.fail(); + }); + it("download all parameters set @loki @sql", async () => { const result = await blobURL.download(Aborter.none, 0, 1, { rangeGetContentMD5: true @@ -128,6 +249,146 @@ describe("BlobAPIs", () => { ); }); + it("delete should work for valid ifMatch @loki @sql", async () => { + const properties = await blobURL.getProperties(Aborter.none); + + const result = await blobURL.delete(Aborter.none, { + blobAccessConditions: { + modifiedAccessConditions: { + ifMatch: properties.eTag + } + } + }); + assert.equal( + result._response.request.headers.get("x-ms-client-request-id"), + result.clientRequestId + ); + }); + + it("delete should work for * ifMatch @loki @sql", async () => { + const result = await blobURL.delete(Aborter.none, { + blobAccessConditions: { + modifiedAccessConditions: { + ifMatch: "*" + } + } + }); + assert.equal( + result._response.request.headers.get("x-ms-client-request-id"), + result.clientRequestId + ); + }); + + it("delete should not work for invalid ifMatch @loki @sql", async () => { + try { + await blobURL.delete(Aborter.none, { + blobAccessConditions: { + modifiedAccessConditions: { + ifMatch: "invalid" + } + } + }); + } catch (error) { + assert.deepStrictEqual(error.statusCode, 412); + return; + } + assert.fail(); + }); + + it("delete should work for valid ifNoneMatch @loki @sql", async () => { + const result = await blobURL.delete(Aborter.none, { + blobAccessConditions: { + modifiedAccessConditions: { + ifNoneMatch: "unmatchetag" + } + } + }); + assert.equal( + result._response.request.headers.get("x-ms-client-request-id"), + result.clientRequestId + ); + }); + + it("delete should not work for invalid ifNoneMatch @loki @sql", async () => { + const properties = await blobURL.getProperties(Aborter.none); + + try { + await blobURL.delete(Aborter.none, { + blobAccessConditions: { + modifiedAccessConditions: { + ifNoneMatch: properties.eTag + } + } + }); + } catch (error) { + assert.deepStrictEqual(error.statusCode, 412); + return; + } + assert.fail(); + }); + + it("delete should work for ifNoneMatch * @loki @sql", async () => { + await blobURL.delete(Aborter.none, { + blobAccessConditions: { + modifiedAccessConditions: { + ifNoneMatch: "*" + } + } + }); + }); + + it("delete should work for valid ifModifiedSince * @loki @sql", async () => { + await blobURL.delete(Aborter.none, { + blobAccessConditions: { + modifiedAccessConditions: { + ifModifiedSince: new Date("2018/01/01") + } + } + }); + }); + + it("delete should not work for invalid ifModifiedSince @loki @sql", async () => { + try { + await blobURL.delete(Aborter.none, { + blobAccessConditions: { + modifiedAccessConditions: { + ifModifiedSince: new Date("2118/01/01") + } + } + }); + } catch (error) { + assert.deepStrictEqual(error.statusCode, 412); + return; + } + assert.fail(); + }); + + it("delete should work for valid ifUnmodifiedSince * @loki @sql", async () => { + await blobURL.delete(Aborter.none, { + blobAccessConditions: { + modifiedAccessConditions: { + ifUnmodifiedSince: new Date("2118/01/01") + } + } + }); + }); + + it("delete should not work for invalid ifUnmodifiedSince @loki @sql", async () => { + try { + await blobURL.delete(Aborter.none, { + blobAccessConditions: { + modifiedAccessConditions: { + ifUnmodifiedSince: new Date("2018/01/01") + } + } + }); + } catch (error) { + assert.deepStrictEqual(error.statusCode, 412); + return; + } + assert.fail(); + }); + it("should create a snapshot from a blob @loki @sql", async () => { const result = await blobURL.createSnapshot(Aborter.none); assert.ok(result.snapshot); diff --git a/tests/blob/apis/blockblob.test.ts b/tests/blob/apis/blockblob.test.ts index 917cbd5c3..1cbe5ad6d 100644 --- a/tests/blob/apis/blockblob.test.ts +++ b/tests/blob/apis/blockblob.test.ts @@ -282,6 +282,44 @@ describe("BlockBlobAPIs", () => { assert.deepStrictEqual(await bodyToString(result, 0), ""); }); + it("commitBlockList with empty list should not work with ifNoneMatch=* for existing blob @loki @sql", async () => { + await blockBlobURL.commitBlockList(Aborter.none, []); + + try { + await blockBlobURL.commitBlockList(Aborter.none, [], { + accessConditions: { + modifiedAccessConditions: { + ifNoneMatch: "*" + } + } + }); + } catch (error) { + assert.deepStrictEqual(error.statusCode, 409); + return; + } + + assert.fail(); + }); + + it("upload should not work with ifNoneMatch=* for existing blob @loki @sql", async () => { + await blockBlobURL.commitBlockList(Aborter.none, []); + + try { + await blockBlobURL.upload(Aborter.none, "hello", 5, { + accessConditions: { + modifiedAccessConditions: { + ifNoneMatch: "*" + } + } + }); + } catch (error) { + assert.deepStrictEqual(error.statusCode, 409); + return; + } + + assert.fail(); + }); + it("commitBlockList with all parameters set @loki @sql", async () => { const body = "HelloWorld"; await blockBlobURL.stageBlock( @@ -383,6 +421,29 @@ describe("BlockBlobAPIs", () => { body, body.length ); + + // Getproperties on a block blob without commited block will return 404 + let err; + try { + await blockBlobURL.getProperties(Aborter.none); + } catch (error) { + err = error; + } + assert.deepStrictEqual(err.statusCode, 404); + + // Stage block with block Id length different than the exist uncommited blocks will fail with 400 + try { + await blockBlobURL.stageBlock( + Aborter.none, + base64encode("123"), + body, + body.length + ); + } catch (error) { + err = error; + } + assert.deepStrictEqual(err.statusCode, 400); + await blockBlobURL.commitBlockList(Aborter.none, [ base64encode("1"), base64encode("2") @@ -390,7 +451,7 @@ describe("BlockBlobAPIs", () => { await blockBlobURL.stageBlock( Aborter.none, - base64encode("3"), + base64encode("123"), body, body.length ); @@ -408,7 +469,7 @@ describe("BlockBlobAPIs", () => { listResponse = await blockBlobURL.getBlockList(Aborter.none, "uncommitted"); assert.equal(listResponse.uncommittedBlocks!.length, 1); - assert.equal(listResponse.uncommittedBlocks![0].name, base64encode("3")); + assert.equal(listResponse.uncommittedBlocks![0].name, base64encode("123")); assert.equal(listResponse.uncommittedBlocks![0].size, body.length); assert.equal(listResponse.committedBlocks!.length, 0); @@ -419,7 +480,7 @@ describe("BlockBlobAPIs", () => { assert.equal(listResponse.committedBlocks![1].name, base64encode("2")); assert.equal(listResponse.committedBlocks![1].size, body.length); assert.equal(listResponse.uncommittedBlocks!.length, 1); - assert.equal(listResponse.uncommittedBlocks![0].name, base64encode("3")); + assert.equal(listResponse.uncommittedBlocks![0].name, base64encode("123")); assert.equal(listResponse.uncommittedBlocks![0].size, body.length); }); diff --git a/tests/blob/apis/container.test.ts b/tests/blob/apis/container.test.ts index 479d400d4..c4f8331d8 100644 --- a/tests/blob/apis/container.test.ts +++ b/tests/blob/apis/container.test.ts @@ -70,6 +70,42 @@ describe("ContainerAPIs", () => { assert.deepEqual(result.metadata, metadata); }); + it("setMetadata should work with conditional headers @loki @sql", async () => { + // const properties = await containerURL.getProperties(Aborter.none); + await containerURL.setMetadata( + Aborter.none, + {}, + { + containerAccessConditions: { + modifiedAccessConditions: { + ifModifiedSince: new Date("2018/01/01") + } + } + } + ); + }); + + it("setMetadata should not work with invalid conditional headers @loki @sql", async () => { + try { + await containerURL.setMetadata( + Aborter.none, + {}, + { + containerAccessConditions: { + modifiedAccessConditions: { + ifModifiedSince: new Date("2118/01/01") + } + } + } + ); + } catch (error) { + assert.deepStrictEqual(error.statusCode, 412); + return; + } + + assert.fail(); + }); + it("getProperties @loki @sql", async () => { const result = await containerURL.getProperties(Aborter.none); assert.ok(result.eTag!.length > 0); diff --git a/tests/blob/apis/pageblob.test.ts b/tests/blob/apis/pageblob.test.ts index acf099183..d91a1aba6 100644 --- a/tests/blob/apis/pageblob.test.ts +++ b/tests/blob/apis/pageblob.test.ts @@ -9,6 +9,7 @@ import { } from "@azure/storage-blob"; import assert = require("assert"); +import { SequenceNumberActionType } from "../../../src/blob/generated/artifacts/models"; import { configLogger } from "../../../src/common/Logger"; import BlobTestServerFactory from "../../BlobTestServerFactory"; import { @@ -133,6 +134,7 @@ describe("PageBlobAPIs", () => { properties.contentType, options.blobHTTPHeaders.blobContentType ); + assert.equal(0, properties.blobSequenceNumber); assert.equal(properties.metadata!.key1, options.metadata.key1); assert.equal(properties.metadata!.key2, options.metadata.key2); assert.equal( @@ -290,6 +292,141 @@ describe("PageBlobAPIs", () => { assert.equal(await bodyToString(page2, 512), "b".repeat(512)); }); + it("uploadPages should work with sequence number conditions @loki", async () => { + await pageBlobURL.create(Aborter.none, 1024); + + await pageBlobURL.updateSequenceNumber( + Aborter.none, + SequenceNumberActionType.Update, + 10 + ); + + const result = await blobURL.download(Aborter.none, 0); + assert.equal(await bodyToString(result, 1024), "\u0000".repeat(1024)); + + await pageBlobURL.uploadPages(Aborter.none, "a".repeat(512), 0, 512, { + accessConditions: { + sequenceNumberAccessConditions: { + ifSequenceNumberEqualTo: 10, + ifSequenceNumberLessThan: 11, + ifSequenceNumberLessThanOrEqualTo: 10 + } + } + }); + const result_upload = await pageBlobURL.uploadPages( + Aborter.none, + "b".repeat(512), + 512, + 512 + ); + assert.equal( + result_upload._response.request.headers.get("x-ms-client-request-id"), + result_upload.clientRequestId + ); + + const page1 = await pageBlobURL.download(Aborter.none, 0, 512); + const page2 = await pageBlobURL.download(Aborter.none, 512, 512); + + assert.equal(await bodyToString(page1, 512), "a".repeat(512)); + assert.equal(await bodyToString(page2, 512), "b".repeat(512)); + }); + + it("uploadPages should not work if ifSequenceNumberEqualTo doesn't match @loki", async () => { + await pageBlobURL.create(Aborter.none, 1024); + + await pageBlobURL.updateSequenceNumber( + Aborter.none, + SequenceNumberActionType.Update, + 10 + ); + + try { + await pageBlobURL.uploadPages(Aborter.none, "a".repeat(512), 0, 512, { + accessConditions: { + sequenceNumberAccessConditions: { + ifSequenceNumberEqualTo: 11 + } + } + }); + } catch (error) { + assert.deepStrictEqual(error.statusCode, 412); + return; + } + + assert.fail(); + }); + + it("uploadPages should not work if ifSequenceNumberLessThan doesn't match @loki", async () => { + await pageBlobURL.create(Aborter.none, 1024); + + await pageBlobURL.updateSequenceNumber( + Aborter.none, + SequenceNumberActionType.Update, + 10 + ); + + try { + await pageBlobURL.uploadPages(Aborter.none, "a".repeat(512), 0, 512, { + accessConditions: { + sequenceNumberAccessConditions: { + ifSequenceNumberLessThan: 10 + } + } + }); + } catch (error) { + assert.deepStrictEqual(error.statusCode, 412); + return; + } + + try { + await pageBlobURL.uploadPages(Aborter.none, "a".repeat(512), 0, 512, { + accessConditions: { + sequenceNumberAccessConditions: { + ifSequenceNumberLessThan: 9 + } + } + }); + } catch (error) { + assert.deepStrictEqual(error.statusCode, 412); + return; + } + + assert.fail(); + }); + + it("uploadPages should not work if ifSequenceNumberLessThanOrEqualTo doesn't match @loki", async () => { + await pageBlobURL.create(Aborter.none, 1024); + + await pageBlobURL.updateSequenceNumber( + Aborter.none, + SequenceNumberActionType.Update, + 10 + ); + + await pageBlobURL.uploadPages(Aborter.none, "a".repeat(512), 0, 512, { + accessConditions: { + sequenceNumberAccessConditions: { + ifSequenceNumberLessThanOrEqualTo: 10 + } + } + }); + + try { + await pageBlobURL.uploadPages(Aborter.none, "a".repeat(512), 0, 512, { + accessConditions: { + sequenceNumberAccessConditions: { + ifSequenceNumberLessThanOrEqualTo: 9 + } + } + }); + } catch (error) { + assert.deepStrictEqual(error.statusCode, 412); + return; + } + + assert.fail(); + }); + it("uploadPages with sequential pages @loki", async () => { const length = 512 * 3; await pageBlobURL.create(Aborter.none, length); @@ -1117,6 +1254,96 @@ describe("PageBlobAPIs", () => { ); }); + it("clearPages should work with sequence number conditions @loki", async () => { + await pageBlobURL.create(Aborter.none, 1024); + await pageBlobURL.clearPages(Aborter.none, 0, 512, { + accessConditions: { + sequenceNumberAccessConditions: { + ifSequenceNumberEqualTo: 0, + ifSequenceNumberLessThan: 1, + ifSequenceNumberLessThanOrEqualTo: 0 + } + } + }); + }); + + it("clearPages should not work with invalid ifSequenceNumberEqualTo @loki", async () => { + await pageBlobURL.create(Aborter.none, 1024); + try { + await pageBlobURL.clearPages(Aborter.none, 0, 512, { + accessConditions: { + sequenceNumberAccessConditions: { + ifSequenceNumberEqualTo: 1 + } + } + }); + } catch (error) { + assert.deepStrictEqual(error.statusCode, 412); + return; + } + assert.fail(); + }); + + it("clearPages should not work with invalid ifSequenceNumberLessThan @loki", async () => { + await pageBlobURL.create(Aborter.none, 1024); + await pageBlobURL.updateSequenceNumber( + Aborter.none, + SequenceNumberActionType.Increment + ); + + await pageBlobURL.clearPages(Aborter.none, 0, 512, { + accessConditions: { + sequenceNumberAccessConditions: { + ifSequenceNumberLessThan: 2 + } + } + }); + + try { + await pageBlobURL.clearPages(Aborter.none, 0, 512, { + accessConditions: { + sequenceNumberAccessConditions: { + ifSequenceNumberLessThan: 1 + } + } + }); + } catch (error) { + assert.deepStrictEqual(error.statusCode, 412); + return; + } + assert.fail(); + }); + + it("clearPages should not work with invalid ifSequenceNumberLessThanOrEqualTo @loki", async () => { + await pageBlobURL.create(Aborter.none, 1024); + await pageBlobURL.updateSequenceNumber( + Aborter.none, + SequenceNumberActionType.Increment + ); + + await pageBlobURL.clearPages(Aborter.none, 0, 512, { + accessConditions: { + sequenceNumberAccessConditions: { + ifSequenceNumberLessThanOrEqualTo: 1 + } + } + }); + + try { + await pageBlobURL.clearPages(Aborter.none, 0, 512, { + accessConditions: { + sequenceNumberAccessConditions: { + ifSequenceNumberLessThanOrEqualTo: 0 + } + } + }); + } catch (error) { + assert.deepStrictEqual(error.statusCode, 412); + return; + } + assert.fail(); + }); + it("clearPages to internally override a sequential range @loki", async () => { const length = 512 * 5; await pageBlobURL.create(Aborter.none, length); diff --git a/tests/blob/blockblob.highlevel.test.ts b/tests/blob/blockblob.highlevel.test.ts index d5fb81607..e5709d338 100644 --- a/tests/blob/blockblob.highlevel.test.ts +++ b/tests/blob/blockblob.highlevel.test.ts @@ -169,6 +169,7 @@ describe("BlockBlobHighlevel", () => { assert.ok(downloadedData.equals(uploadedData)); }); + // tslint:disable-next-line: max-line-length it("uploadFileToBlockBlob should update progress when blob >= BLOCK_BLOB_MAX_UPLOAD_BLOB_BYTES @loki @sql", async () => { let eventTriggered = false; const aborter = Aborter.none; @@ -187,6 +188,7 @@ describe("BlockBlobHighlevel", () => { assert.ok(eventTriggered); }); + // tslint:disable-next-line: max-line-length it("uploadFileToBlockBlob should update progress when blob < BLOCK_BLOB_MAX_UPLOAD_BLOB_BYTES @loki @sql", async () => { let eventTriggered = false; const aborter = Aborter.none; @@ -403,6 +405,7 @@ describe("BlockBlobHighlevel", () => { assert.ok(downloadedData.equals(uploadedData)); }); + // tslint:disable-next-line: max-line-length it("bloburl.download should download full data successfully when internal stream unexpected ends @loki @sql", async () => { await uploadFileToBlockBlob(Aborter.none, tempFileSmall, blockBlobURL, { blockSize: 4 * 1024 * 1024, diff --git a/tests/blob/conditions.test.ts b/tests/blob/conditions.test.ts new file mode 100644 index 000000000..a155b4217 --- /dev/null +++ b/tests/blob/conditions.test.ts @@ -0,0 +1,1008 @@ +import assert = require("assert"); + +import ConditionalHeadersAdapter from "../../src/blob/conditions/ConditionalHeadersAdapter"; +import ConditionResourceAdapter from "../../src/blob/conditions/ConditionResourceAdapter"; +import ReadConditionalHeadersValidator from "../../src/blob/conditions/ReadConditionalHeadersValidator"; +import WriteConditionalHeadersValidator from "../../src/blob/conditions/WriteConditionalHeadersValidator"; +import StorageErrorFactory from "../../src/blob/errors/StorageErrorFactory"; +import Context from "../../src/blob/generated/Context"; +import { + BlobModel, + ContainerModel +} from "../../src/blob/persistence/IBlobMetadataStore"; + +const context = { contextId: "" } as Context; + +describe("ConditionalHeadersAdapter", () => { + it("Should work with undefined values @loki @sql", () => { + const validator = new ConditionalHeadersAdapter(context); + assert.deepStrictEqual(validator.ifModifiedSince, undefined); + assert.deepStrictEqual(validator.ifUnmodifiedSince, undefined); + assert.deepStrictEqual(validator.ifMatch, undefined); + assert.deepStrictEqual(validator.ifNoneMatch, undefined); + }); + + it("Should work with single etags @loki @sql", () => { + const modifiedAccessConditions = { + ifModifiedSince: new Date("2020/01/01"), + ifUnmodifiedSince: new Date("2020/02/01"), + ifMatch: "etag1", + ifNoneMatch: "etag2" + }; + + const validator = new ConditionalHeadersAdapter( + context, + modifiedAccessConditions + ); + assert.deepStrictEqual( + validator.ifModifiedSince, + modifiedAccessConditions.ifModifiedSince + ); + assert.deepStrictEqual( + validator.ifUnmodifiedSince, + modifiedAccessConditions.ifUnmodifiedSince + ); + assert.deepStrictEqual(validator.ifMatch, [ + modifiedAccessConditions.ifMatch + ]); + assert.deepStrictEqual(validator.ifNoneMatch, [ + modifiedAccessConditions.ifNoneMatch + ]); + }); + + it("Should work with multi etags @loki @sql", () => { + const modifiedAccessConditions = { + ifModifiedSince: new Date("2020/01/01"), + ifUnmodifiedSince: new Date("2020/02/01"), + ifMatch: "etag1,etag2", + ifNoneMatch: "etag3,etag4,etag5" + }; + + const validator = new ConditionalHeadersAdapter( + context, + modifiedAccessConditions + ); + assert.deepStrictEqual( + validator.ifModifiedSince, + modifiedAccessConditions.ifModifiedSince + ); + assert.deepStrictEqual( + validator.ifUnmodifiedSince, + modifiedAccessConditions.ifUnmodifiedSince + ); + assert.deepStrictEqual(validator.ifMatch, ["etag1", "etag2"]); + assert.deepStrictEqual(validator.ifNoneMatch, ["etag3", "etag4", "etag5"]); + }); + + it("Should work with etags with quotes @loki @sql", () => { + const modifiedAccessConditions = { + ifModifiedSince: new Date("2020/01/01"), + ifUnmodifiedSince: new Date("2020/02/01"), + ifMatch: '"etag1","etag2"', + ifNoneMatch: 'etag3,"etag4",etag5' + }; + + const validator = new ConditionalHeadersAdapter( + context, + modifiedAccessConditions + ); + assert.deepStrictEqual( + validator.ifModifiedSince, + modifiedAccessConditions.ifModifiedSince + ); + assert.deepStrictEqual( + validator.ifUnmodifiedSince, + modifiedAccessConditions.ifUnmodifiedSince + ); + assert.deepStrictEqual(validator.ifMatch, ["etag1", "etag2"]); + assert.deepStrictEqual(validator.ifNoneMatch, ["etag3", "etag4", "etag5"]); + }); +}); + +describe("ConditionResourceAdapter", () => { + it("Should work with undefined or null resource @loki @sql", () => { + const conditionResourceAdapterUndefined = new ConditionResourceAdapter( + undefined + ); + assert.deepStrictEqual(conditionResourceAdapterUndefined.exist, false); + + const conditionResourceAdapterNull = new ConditionResourceAdapter(null); + assert.deepStrictEqual(conditionResourceAdapterNull.exist, false); + }); + + it("Should work with blob model @loki @sql", () => { + const blobModel = { + properties: { + etag: "etag1", + lastModified: new Date("2018/01/01") + } + } as BlobModel; + + const conditionResourceAdapter = new ConditionResourceAdapter(blobModel); + assert.deepStrictEqual(conditionResourceAdapter.exist, true); + assert.deepStrictEqual(conditionResourceAdapter.etag, "etag1"); + assert.deepStrictEqual( + conditionResourceAdapter.lastModified, + blobModel.properties.lastModified + ); + }); + + it("Should work with container model @loki @sql", () => { + const blobModel = { + properties: { + etag: "etag1", + lastModified: new Date() + } + } as ContainerModel; + + const conditionResourceAdapter = new ConditionResourceAdapter(blobModel); + assert.deepStrictEqual(conditionResourceAdapter.exist, true); + assert.deepStrictEqual(conditionResourceAdapter.etag, "etag1"); + + blobModel.properties.lastModified.setMilliseconds(0); + assert.deepStrictEqual( + conditionResourceAdapter.lastModified, + blobModel.properties.lastModified + ); + }); + + it("Should work with etag with quotes @loki @sql", () => { + const blobModel = { + properties: { + etag: '"etag1"', + lastModified: new Date() + } + } as ContainerModel; + + const conditionResourceAdapter = new ConditionResourceAdapter(blobModel); + assert.deepStrictEqual(conditionResourceAdapter.exist, true); + assert.deepStrictEqual(conditionResourceAdapter.etag, "etag1"); + + blobModel.properties.lastModified.setMilliseconds(0); + assert.deepStrictEqual( + conditionResourceAdapter.lastModified, + blobModel.properties.lastModified + ); + }); +}); + +describe("ReadConditionalHeadersValidator for exist resource", () => { + it("Should return 412 preconditoin failed for failed if-match results @loki @sql", () => { + const validator = new ReadConditionalHeadersValidator(); + const modifiedAccessConditions = { ifMatch: "etag1" }; + const blobModel = { + properties: { + etag: "etag2", + lastModified: new Date() + } + } as BlobModel; + + const expectedError = StorageErrorFactory.getConditionNotMet( + context.contextId! + ); + try { + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + } catch (error) { + assert.deepStrictEqual(error.statusCode, expectedError.statusCode); + assert.deepStrictEqual( + error.storageErrorCode, + expectedError.storageErrorCode + ); + assert.deepStrictEqual( + error.storageErrorMessage, + expectedError.storageErrorMessage + ); + return; + } + + assert.fail(); + }); + + it("Should not return 412 preconditoin failed for successful if-match results @loki @sql", () => { + const validator = new ReadConditionalHeadersValidator(); + const modifiedAccessConditions = { ifMatch: "etag1" }; + const blobModel = { + properties: { + etag: "etag1", + lastModified: new Date() + } + } as BlobModel; + + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + }); + + it("Should return 304 Not Modified for failed if-none-match results @loki @sql", () => { + const validator = new ReadConditionalHeadersValidator(); + const modifiedAccessConditions = { ifNoneMatch: 'etag0,"etag1"' }; + const blobModel = { + properties: { + etag: "etag1", + lastModified: new Date() + } + } as BlobModel; + + const expectedError = StorageErrorFactory.getNotModified( + context.contextId! + ); + try { + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + } catch (error) { + assert.deepStrictEqual(error.statusCode, expectedError.statusCode); + assert.deepStrictEqual( + error.storageErrorCode, + expectedError.storageErrorCode + ); + assert.deepStrictEqual( + error.storageErrorMessage, + expectedError.storageErrorMessage + ); + return; + } + + assert.fail(); + }); + + it("Should not return 304 Not Modified for successful if-none-match results @loki @sql", () => { + const validator = new ReadConditionalHeadersValidator(); + const modifiedAccessConditions = { ifNoneMatch: 'etag0,"etag3"' }; + const blobModel = { + properties: { + etag: "etag1", + lastModified: new Date() + } + } as BlobModel; + + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + }); + + it("Should return 304 Not Modified for failed if-modified-since results @loki @sql", () => { + const validator = new ReadConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifModifiedSince: new Date("2019/01/01") + }; + const blobModel = { + properties: { + etag: "etag1", + lastModified: new Date("2018/01/01") + } + } as BlobModel; + + const expectedError = StorageErrorFactory.getNotModified( + context.contextId! + ); + try { + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + } catch (error) { + assert.deepStrictEqual(error.statusCode, expectedError.statusCode); + assert.deepStrictEqual( + error.storageErrorCode, + expectedError.storageErrorCode + ); + assert.deepStrictEqual( + error.storageErrorMessage, + expectedError.storageErrorMessage + ); + return; + } + + assert.fail(); + }); + + it("Should return 304 Not Modified when if-modified-since same with lastModified @loki @sql", () => { + const validator = new ReadConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifModifiedSince: new Date("2019/01/01") + }; + const blobModel = { + properties: { + etag: "etag1", + lastModified: new Date("2019/01/01") + } + } as BlobModel; + + const expectedError = StorageErrorFactory.getNotModified( + context.contextId! + ); + try { + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + } catch (error) { + assert.deepStrictEqual(error.statusCode, expectedError.statusCode); + assert.deepStrictEqual( + error.storageErrorCode, + expectedError.storageErrorCode + ); + assert.deepStrictEqual( + error.storageErrorMessage, + expectedError.storageErrorMessage + ); + return; + } + + assert.fail(); + }); + + it("Should not return 304 Not Modified for successful if-modified-since results @loki @sql", () => { + const validator = new ReadConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifModifiedSince: new Date("2019/01/01") + }; + const blobModel = { + properties: { + etag: "etag1", + lastModified: new Date("2020/01/01") + } + } as BlobModel; + + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + }); + + it("Should return 412 preconditoin failed for failed if-unmodified-since results @loki @sql", () => { + const validator = new ReadConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifUnmodifiedSince: new Date("2019/01/01") + }; + const blobModel = { + properties: { + etag: "etag2", + lastModified: new Date("2020/01/01") + } + } as BlobModel; + + const expectedError = StorageErrorFactory.getConditionNotMet( + context.contextId! + ); + try { + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + } catch (error) { + assert.deepStrictEqual(error.statusCode, expectedError.statusCode); + assert.deepStrictEqual( + error.storageErrorCode, + expectedError.storageErrorCode + ); + assert.deepStrictEqual( + error.storageErrorMessage, + expectedError.storageErrorMessage + ); + return; + } + + assert.fail(); + }); + + it("Should not return 412 preconditoin failed when if-unmodified-since same with lastModified @loki @sql", () => { + const validator = new ReadConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifUnmodifiedSince: new Date("2019/01/01") + }; + const blobModel = { + properties: { + etag: "etag2", + lastModified: new Date("2019/01/01") + } + } as BlobModel; + + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + }); + + it("Should not return 412 preconditoin failed for successful if-unmodified-since results @loki @sql", () => { + const validator = new ReadConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifUnmodifiedSince: new Date("2019/01/01") + }; + const blobModel = { + properties: { + etag: "etag2", + lastModified: new Date("2018/01/01") + } + } as BlobModel; + + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + }); + + it("Should return 200 OK when all conditions match @loki @sql", () => { + const validator = new ReadConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifMatch: "etag1", + ifNoneMatch: "etag3", + ifModifiedSince: new Date("2018/01/01"), + ifUnmodifiedSince: new Date("2020/01/01") + }; + const blobModel = { + properties: { + etag: "etag1", + lastModified: new Date("2019/01/01") + } + } as BlobModel; + + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + }); + + it("Should return 412 Precondition Failed when if-none-match and if-unmodified-since fail among all conditions @loki @sql", () => { + const validator = new ReadConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifMatch: "etag1", + ifNoneMatch: '"etag1"', + ifModifiedSince: new Date("2018/01/01"), + ifUnmodifiedSince: new Date("2020/01/01") + }; + const blobModel = { + properties: { + etag: "etag1", + lastModified: new Date("2021/01/01") + } + } as BlobModel; + + const expectedError = StorageErrorFactory.getConditionNotMet( + context.contextId! + ); + + try { + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + } catch (error) { + assert.deepStrictEqual(error.statusCode, expectedError.statusCode); + assert.deepStrictEqual( + error.storageErrorCode, + expectedError.storageErrorCode + ); + assert.deepStrictEqual( + error.storageErrorMessage, + expectedError.storageErrorMessage + ); + return; + } + + assert.fail(); + }); + + it("Should return 200 OK when if-none-match fails all conditions @loki @sql", () => { + const validator = new ReadConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifMatch: "etag1", + ifNoneMatch: "etag1", + ifModifiedSince: new Date("2018/01/01"), + ifUnmodifiedSince: new Date("2020/01/01") + }; + const blobModel = { + properties: { + etag: "etag1", + lastModified: new Date("2019/01/01") + } + } as BlobModel; + + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + }); + + it("Should return 412 Precondition Failed when if-match and if-modified-since fail among all conditions @loki @sql", () => { + const validator = new ReadConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifMatch: "etag1", + ifNoneMatch: '"etag2"', + ifModifiedSince: new Date("2018/01/01"), + ifUnmodifiedSince: new Date("2020/01/01") + }; + const blobModel = { + properties: { + etag: "etag0", + lastModified: new Date("2017/01/01") + } + } as BlobModel; + + const expectedError = StorageErrorFactory.getConditionNotMet( + context.contextId! + ); + + try { + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + } catch (error) { + assert.deepStrictEqual(error.statusCode, expectedError.statusCode); + assert.deepStrictEqual( + error.storageErrorCode, + expectedError.storageErrorCode + ); + assert.deepStrictEqual( + error.storageErrorMessage, + expectedError.storageErrorMessage + ); + return; + } + + assert.fail(); + }); + + it("Should return 200 OK when if-modified-since fails all conditions @loki @sql", () => { + const validator = new ReadConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifMatch: "etag1", + ifNoneMatch: "etag3", + ifModifiedSince: new Date("2018/01/01"), + ifUnmodifiedSince: new Date("2020/01/01") + }; + const blobModel = { + properties: { + etag: "etag1", + lastModified: new Date("2017/01/01") + } + } as BlobModel; + + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + }); + + it("Should return 304 Not Modified when if-none-match and if-modified-since fail @loki @sql", () => { + const validator = new ReadConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifNoneMatch: '"etag1"', + ifModifiedSince: new Date("2018/01/01") + }; + const blobModel = { + properties: { + etag: "etag1", + lastModified: new Date("2017/01/01") + } + } as BlobModel; + + const expectedError = StorageErrorFactory.getNotModified( + context.contextId! + ); + + try { + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + } catch (error) { + assert.deepStrictEqual(error.statusCode, expectedError.statusCode); + assert.deepStrictEqual( + error.storageErrorCode, + expectedError.storageErrorCode + ); + assert.deepStrictEqual( + error.storageErrorMessage, + expectedError.storageErrorMessage + ); + return; + } + + assert.fail(); + }); +}); + +describe("ReadConditionalHeadersValidator for unexist resource", () => { + it("Should return 412 Precondition Failed for any ifMatch @loki @sql", () => { + const validator = new ReadConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifMatch: "etag1" + }; + + const expectedError = StorageErrorFactory.getConditionNotMet( + context.contextId! + ); + + try { + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(undefined) + ); + } catch (error) { + assert.deepStrictEqual(error.statusCode, expectedError.statusCode); + assert.deepStrictEqual( + error.storageErrorCode, + expectedError.storageErrorCode + ); + assert.deepStrictEqual( + error.storageErrorMessage, + expectedError.storageErrorMessage + ); + return; + } + + assert.fail(); + }); + + it("Should return 400 getUnsatisfiableCondition for if none-match value * @loki @sql", () => { + const validator = new ReadConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifNoneMatch: "*" + }; + + const expectedError = StorageErrorFactory.getUnsatisfiableCondition( + context.contextId! + ); + + try { + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(undefined) + ); + } catch (error) { + assert.deepStrictEqual(error.statusCode, expectedError.statusCode); + assert.deepStrictEqual( + error.storageErrorCode, + expectedError.storageErrorCode + ); + assert.deepStrictEqual( + error.storageErrorMessage, + expectedError.storageErrorMessage + ); + return; + } + + assert.fail(); + }); +}); + +describe("WriteConditionalHeadersValidator for unexist resource", () => { + it("Should throw 400 Bad Request for invalid combinations conditional headers @loki @sql", () => { + const validator = new WriteConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifMatch: "etag1", + ifNoneMatch: '"etag2"', + ifModifiedSince: new Date("2018/01/01"), + ifUnmodifiedSince: new Date("2020/01/01") + }; + + const expectedError = StorageErrorFactory.getMultipleConditionHeadersNotSupported( + context.contextId! + ); + + try { + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(undefined) + ); + } catch (error) { + assert.deepStrictEqual(error.statusCode, expectedError.statusCode); + assert.deepStrictEqual( + error.storageErrorCode, + expectedError.storageErrorCode + ); + assert.deepStrictEqual( + error.storageErrorMessage, + expectedError.storageErrorMessage + ); + return; + } + + assert.fail(); + }); + + it("Should throw 400 Bad Request for multi etags in ifMatch @loki @sql", () => { + const validator = new WriteConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifMatch: "etag1,etag2", + ifNoneMatch: '"etag2"', + ifModifiedSince: new Date("2018/01/01"), + ifUnmodifiedSince: new Date("2020/01/01") + }; + + const expectedError = StorageErrorFactory.getMultipleConditionHeadersNotSupported( + context.contextId! + ); + + try { + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(undefined) + ); + } catch (error) { + assert.deepStrictEqual(error.statusCode, expectedError.statusCode); + assert.deepStrictEqual( + error.storageErrorCode, + expectedError.storageErrorCode + ); + assert.deepStrictEqual( + error.storageErrorMessage, + expectedError.storageErrorMessage + ); + return; + } + + assert.fail(); + }); + + it("Should throw 400 Bad Request for multi etags in if-none-match @loki @sql", () => { + const validator = new WriteConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifMatch: "etag2", + ifNoneMatch: '"etag2",*', + ifModifiedSince: new Date("2018/01/01"), + ifUnmodifiedSince: new Date("2020/01/01") + }; + + const expectedError = StorageErrorFactory.getMultipleConditionHeadersNotSupported( + context.contextId! + ); + + try { + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(undefined) + ); + } catch (error) { + assert.deepStrictEqual(error.statusCode, expectedError.statusCode); + assert.deepStrictEqual( + error.storageErrorCode, + expectedError.storageErrorCode + ); + assert.deepStrictEqual( + error.storageErrorMessage, + expectedError.storageErrorMessage + ); + return; + } + + assert.fail(); + }); + + it("Should throw 412 Precondition Failed for any values in if-match @loki @sql", () => { + const validator = new WriteConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifMatch: "etag2" + }; + + const expectedError = StorageErrorFactory.getConditionNotMet( + context.contextId! + ); + + try { + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(undefined) + ); + } catch (error) { + assert.deepStrictEqual(error.statusCode, expectedError.statusCode); + assert.deepStrictEqual( + error.storageErrorCode, + expectedError.storageErrorCode + ); + assert.deepStrictEqual( + error.storageErrorMessage, + expectedError.storageErrorMessage + ); + return; + } + + assert.fail(); + }); +}); + +describe("WriteConditionalHeadersValidator for exist resource", () => { + it("Should return 200 for successful if-none-match and failed if-modified-since @loki @sql", () => { + const validator = new WriteConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifNoneMatch: '"etag2"', + ifModifiedSince: new Date("2018/01/01") + }; + + const blobModel = { + properties: { + etag: "etag1", + lastModified: new Date("2017/01/01") + } + } as BlobModel; + + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + }); + + it("Should return 200 for successful if-match and failed if-unmodified-since @loki @sql", () => { + const validator = new WriteConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifMatch: '"etag1"', + ifUnmodifiedSince: new Date("2018/01/01") + }; + + const blobModel = { + properties: { + etag: "etag1", + lastModified: new Date("2019/01/01") + } + } as BlobModel; + + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + }); + + it("Should return 200 for if-unmodified-since equal with lastModified @loki @sql", () => { + const validator = new WriteConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifUnmodifiedSince: new Date() + }; + + const blobModel = { + properties: { + etag: "etag1", + lastModified: modifiedAccessConditions.ifUnmodifiedSince + } + } as BlobModel; + + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + }); + + it("Should return 412 for if-modified-since equal with lastModifiedSince @loki @sql", () => { + const validator = new WriteConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifModifiedSince: new Date() + }; + const blobModel = { + properties: { + etag: "etag1", + lastModified: modifiedAccessConditions.ifModifiedSince + } + } as BlobModel; + + const expectedError = StorageErrorFactory.getConditionNotMet( + context.contextId! + ); + try { + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + } catch (error) { + assert.deepStrictEqual(error.statusCode, expectedError.statusCode); + assert.deepStrictEqual( + error.storageErrorCode, + expectedError.storageErrorCode + ); + assert.deepStrictEqual( + error.storageErrorMessage, + expectedError.storageErrorMessage + ); + return; + } + + assert.fail(); + }); + + it("Should return 412 for failed if-modified-since results @loki @sql", () => { + const validator = new WriteConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifModifiedSince: new Date("2019/01/01") + }; + const blobModel = { + properties: { + etag: "etag1", + lastModified: new Date("2018/01/01") + } + } as BlobModel; + + const expectedError = StorageErrorFactory.getConditionNotMet( + context.contextId! + ); + try { + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + } catch (error) { + assert.deepStrictEqual(error.statusCode, expectedError.statusCode); + assert.deepStrictEqual( + error.storageErrorCode, + expectedError.storageErrorCode + ); + assert.deepStrictEqual( + error.storageErrorMessage, + expectedError.storageErrorMessage + ); + return; + } + + assert.fail(); + }); + + it("Should return 412 for failed if-unmodified-since results @loki @sql", () => { + const validator = new WriteConditionalHeadersValidator(); + const modifiedAccessConditions = { + ifUnmodifiedSince: new Date("2019/01/01") + }; + const blobModel = { + properties: { + etag: "etag1", + lastModified: new Date("2020/01/01") + } + } as BlobModel; + + const expectedError = StorageErrorFactory.getConditionNotMet( + context.contextId! + ); + try { + validator.validate( + context, + new ConditionalHeadersAdapter(context, modifiedAccessConditions), + new ConditionResourceAdapter(blobModel) + ); + } catch (error) { + assert.deepStrictEqual(error.statusCode, expectedError.statusCode); + assert.deepStrictEqual( + error.storageErrorCode, + expectedError.storageErrorCode + ); + assert.deepStrictEqual( + error.storageErrorMessage, + expectedError.storageErrorMessage + ); + return; + } + + assert.fail(); + }); +}); diff --git a/tests/blob/specialnaming.test.ts b/tests/blob/specialnaming.test.ts index 5e4703d3d..52cdda49b 100644 --- a/tests/blob/specialnaming.test.ts +++ b/tests/blob/specialnaming.test.ts @@ -1,3 +1,5 @@ +import dns = require("dns"); + import { Aborter, BlockBlobURL, @@ -25,6 +27,8 @@ describe("SpecialNaming", () => { const server = factory.createServer(); const baseURL = `http://${server.config.host}:${server.config.port}/devstoreaccount1`; + const productionStyleHostName = "devstoreaccount1.localhost"; // Use hosts file to make this resolve + const serviceURL = new ServiceURL( baseURL, StorageURL.newPipeline( @@ -438,4 +442,56 @@ describe("SpecialNaming", () => { ); assert.notDeepEqual(response.segment.blobItems.length, 0); }); + + it(`Should work with production style URL when ${productionStyleHostName} is resolvable`, async () => { + await dns.promises.lookup(productionStyleHostName).then( + async lookupAddress => { + const baseURLProductionStyle = `http://${productionStyleHostName}:${server.config.port}`; + const serviceURLProductionStyle = new ServiceURL( + baseURLProductionStyle, + StorageURL.newPipeline( + new SharedKeyCredential( + EMULATOR_ACCOUNT_NAME, + EMULATOR_ACCOUNT_KEY + ), + { + retryOptions: { maxTries: 1 } + } + ) + ); + const containerURLProductionStyle = ContainerURL.fromServiceURL( + serviceURLProductionStyle, + containerName + ); + + const blobName: string = getUniqueName("myblob"); + const blockBlobURL = BlockBlobURL.fromContainerURL( + containerURLProductionStyle, + blobName + ); + + await blockBlobURL.upload(Aborter.none, "ABC", 3); + const response = await containerURLProductionStyle.listBlobHierarchySegment( + Aborter.none, + "$", + undefined, + { + prefix: blobName + } + ); + assert.notDeepEqual(response.segment.blobItems.length, 0); + }, + () => { + // Cannot perform this test. We need devstoreaccount1.localhost to resolve to 127.0.0.1. + // On Linux, this should just work, + // On Windows, we can't spoof DNS record for specific process. + // So we have options of running our own DNS server (overkill), + // or editing hosts files (machine global operation; and requires running as admin). + // So skip the test case. + assert.ok( + `Skipping test case - it needs ${productionStyleHostName} to be resolvable` + ); + } + ); + }); }); diff --git a/tests/blob/utils.test.ts b/tests/blob/utils.test.ts new file mode 100644 index 000000000..382f2ce02 --- /dev/null +++ b/tests/blob/utils.test.ts @@ -0,0 +1,56 @@ +import assert = require("assert"); +import { convertRawHeadersToMetadata } from "../../src/common/utils/utils"; + +describe("Utils", () => { + it("convertRawHeadersToMetadata should work", () => { + // upper case, lower case keys/values + const metadata = convertRawHeadersToMetadata([ + "x-ms-meta-Name1", + "Value", + "x-ms-meta-name2", + "234", + "x-ms-meta-name1", + "Value" + ]); + assert.deepStrictEqual(metadata, { + Name1: "Value", + name2: "234", + name1: "Value" + }); + }); + + it("convertRawHeadersToMetadata should work with duplicated metadata", () => { + const metadata = convertRawHeadersToMetadata([ + "x-ms-meta-name1", + "Value", + "x-ms-meta-name1", + "234" + ]); + assert.deepStrictEqual(metadata, { + name1: "Value,234" + }); + }); + + it("convertRawHeadersToMetadata should work with empty metadata", () => { + const metadata = convertRawHeadersToMetadata([ + "x-ms-meta-Name1", + "", + "x-ms-meta-name1", + "234" + ]); + assert.deepStrictEqual(metadata, { + Name1: "", + name1: "234" + }); + }); + + it("convertRawHeadersToMetadata should work with empty raw headers", () => { + const metadata = convertRawHeadersToMetadata(); + assert.deepStrictEqual(metadata, undefined); + }); + + it("convertRawHeadersToMetadata should work with empty raw headers array", () => { + const metadata = convertRawHeadersToMetadata([]); + assert.deepStrictEqual(metadata, undefined); + }); +});