├── .deployment ├── .gitignore ├── LICENSE ├── README.md ├── azuredeploy.json ├── azuredeploy.portal.json └── src ├── StaticFileServer ├── function.json ├── project.json └── run.csx ├── host.json ├── proxies.json └── www ├── assets └── images │ └── Azure-Functions-Logo.png └── index.html /.deployment: -------------------------------------------------------------------------------- 1 | [config] 2 | project = src 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/macos,visualstudiocode 2 | 3 | ### macOS ### 4 | *.DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | 12 | # Thumbnails 13 | ._* 14 | 15 | # Files that might appear in the root of a volume 16 | .DocumentRevisions-V100 17 | .fseventsd 18 | .Spotlight-V100 19 | .TemporaryItems 20 | .Trashes 21 | .VolumeIcon.icns 22 | .com.apple.timemachine.donotpresent 23 | 24 | # Directories potentially created on remote AFP share 25 | .AppleDB 26 | .AppleDesktop 27 | Network Trash Folder 28 | Temporary Items 29 | .apdisk 30 | 31 | ### VisualStudioCode ### 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # End of https://www.gitignore.io/api/macos,visualstudiocode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Anthony Chu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azure Functions Static File Server 2 | 3 | [](https://azuredeploy.net/) 4 | 5 | Serve a static website with Azure Functions. 6 | 7 | More details at: http://anthonychu.ca/post/azure-functions-static-file-server 8 | 9 | -------------------------------------------------------------------------------- /azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "parameters": { 3 | "siteName": { 4 | "type": "string", 5 | "metadata": { 6 | "description": "The name of the function app that you wish to create." 7 | } 8 | }, 9 | "siteLocation": { 10 | "type": "string", 11 | "defaultValue": "West US", 12 | "metadata": { 13 | "description": "The location to use for creating the function app and hosting plan. It must be one of the Azure locations that support function apps." 14 | } 15 | }, 16 | "defaultPage": { 17 | "type": "string", 18 | "defaultValue": "index.html", 19 | "metadata": { 20 | "description": "Default web page to show when directory is accessed." 21 | } 22 | } 23 | }, 24 | "variables": { 25 | "storageName": "[concat('function', uniqueString(parameters('siteName')))]", 26 | "contentShareName": "[toLower(parameters('siteName'))]", 27 | "repoUrl": "https://github.com/anthonychu/azure-functions-static-file-server", 28 | "branch": "master" 29 | }, 30 | "resources": [ 31 | { 32 | "apiVersion": "2016-03-01", 33 | "name": "[parameters('siteName')]", 34 | "type": "Microsoft.Web/sites", 35 | "properties": { 36 | "name": "[parameters('siteName')]", 37 | "siteConfig": { 38 | "appSettings": [ 39 | { 40 | "name": "AzureWebJobsDashboard", 41 | "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageName')), '2015-05-01-preview').key1)]" 42 | }, 43 | { 44 | "name": "AzureWebJobsStorage", 45 | "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageName')), '2015-05-01-preview').key1)]" 46 | }, 47 | { 48 | "name": "FUNCTIONS_EXTENSION_VERSION", 49 | "value": "~1" 50 | }, 51 | { 52 | "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", 53 | "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageName')), '2015-05-01-preview').key1)]" 54 | }, 55 | { 56 | "name": "WEBSITE_CONTENTSHARE", 57 | "value": "[variables('contentShareName')]" 58 | }, 59 | { 60 | "name": "DEFAULT_PAGE", 61 | "value": "[parameters('defaultPage')]" 62 | }, 63 | { 64 | "name": "ROUTING_EXTENSION_VERSION", 65 | "value": "~0.1" 66 | } 67 | ] 68 | }, 69 | "clientAffinityEnabled": false 70 | }, 71 | "resources": [ 72 | { 73 | "apiVersion": "2015-08-01", 74 | "name": "web", 75 | "type": "sourcecontrols", 76 | "dependsOn": [ 77 | "[resourceId('Microsoft.Web/Sites', parameters('siteName'))]" 78 | ], 79 | "properties": { 80 | "RepoUrl": "[variables('repoURL')]", 81 | "branch": "[variables('branch')]", 82 | "IsManualIntegration": true 83 | } 84 | } 85 | ], 86 | "dependsOn": [ 87 | "[resourceId('Microsoft.Storage/storageAccounts', variables('storageName'))]" 88 | ], 89 | "location": "[parameters('siteLocation')]", 90 | "kind": "functionapp" 91 | }, 92 | { 93 | "apiVersion": "2015-05-01-preview", 94 | "type": "Microsoft.Storage/storageAccounts", 95 | "name": "[variables('storageName')]", 96 | "location": "[parameters('siteLocation')]", 97 | "properties": { 98 | "accountType": "Standard_LRS" 99 | }, 100 | "resources": [ 101 | { 102 | "type": "Microsoft.Storage/storageAccounts/providers/locks", 103 | "name": "[concat(variables('storageName'), '/Microsoft.Authorization/', variables('storageName'))]", 104 | "apiVersion": "2015-01-01", 105 | "dependsOn": [ 106 | "[concat('Microsoft.Storage/storageAccounts/', variables('storageName'))]" 107 | ], 108 | "properties": { 109 | "level": "CannotDelete", 110 | "notes": "One or more function apps were linked to this storage account. You can see all the function apps linked to the account under 'files' or 'shares'." 111 | } 112 | } 113 | ] 114 | } 115 | ], 116 | "outputs": { 117 | "siteUri": { 118 | "type": "string", 119 | "value": "[concat('https://',reference(resourceId('Microsoft.Web/sites', parameters('siteName'))).hostNames[0])]" 120 | } 121 | }, 122 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 123 | "contentVersion": "1.0.0.0" 124 | } 125 | -------------------------------------------------------------------------------- /azuredeploy.portal.json: -------------------------------------------------------------------------------- 1 | { 2 | "parameters": { 3 | "appName": { 4 | "type": "string", 5 | "metadata": { 6 | "description": "The name of the function app that you wish to create." 7 | } 8 | }, 9 | "defaultPage": { 10 | "type": "string", 11 | "defaultValue": "index.html", 12 | "metadata": { 13 | "description": "Default web page to show when directory is accessed." 14 | } 15 | } 16 | }, 17 | "resources": [ 18 | { 19 | "apiVersion": "2015-01-01", 20 | "name": "linkedTemplate", 21 | "type": "Microsoft.Resources/deployments", 22 | "properties": { 23 | "mode": "Incremental", 24 | "templateLink": { 25 | "uri": "https://raw.githubusercontent.com/anthonychu/azure-functions-static-file-server/master/azuredeploy.json", 26 | "contentVersion": "1.0.0.0" 27 | }, 28 | "parameters": { 29 | "siteName": { 30 | "value": "[parameters('appName')]" 31 | }, 32 | "defaultPage": { 33 | "value": "[parameters('defaultPage')]" 34 | }, 35 | "siteLocation": { 36 | "value": "[resourceGroup().location]" 37 | } 38 | } 39 | } 40 | } 41 | ], 42 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 43 | "contentVersion": "1.0.0.0" 44 | } 45 | -------------------------------------------------------------------------------- /src/StaticFileServer/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "name": "req", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "methods": [ 9 | "get" 10 | ] 11 | }, 12 | { 13 | "name": "$return", 14 | "type": "http", 15 | "direction": "out" 16 | } 17 | ], 18 | "disabled": false 19 | } 20 | -------------------------------------------------------------------------------- /src/StaticFileServer/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "frameworks": { 3 | "net46":{ 4 | "dependencies": { 5 | "MediaTypeMap": "2.1.0" 6 | } 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/StaticFileServer/run.csx: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http.Headers; 3 | using System.Threading.Tasks; 4 | using System.IO; 5 | using MimeTypes; 6 | 7 | const string staticFilesFolder = "www"; 8 | static string defaultPage = string.IsNullOrEmpty(GetEnvironmentVariable("DEFAULT_PAGE")) ? 9 | "index.html" : GetEnvironmentVariable("DEFAULT_PAGE"); 10 | 11 | public static HttpResponseMessage Run(HttpRequestMessage req, TraceWriter log) 12 | { 13 | try 14 | { 15 | var filePath = GetFilePath(req, log); 16 | 17 | var response = new HttpResponseMessage(HttpStatusCode.OK); 18 | var stream = new FileStream(filePath, FileMode.Open); 19 | response.Content = new StreamContent(stream); 20 | response.Content.Headers.ContentType = new MediaTypeHeaderValue(GetMimeType(filePath)); 21 | return response; 22 | } 23 | catch 24 | { 25 | return new HttpResponseMessage(HttpStatusCode.NotFound); 26 | } 27 | } 28 | 29 | private static string GetScriptPath() 30 | => Path.Combine(GetEnvironmentVariable("HOME"), @"site\wwwroot"); 31 | 32 | private static string GetEnvironmentVariable(string name) 33 | => System.Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.Process); 34 | 35 | private static string GetFilePath(HttpRequestMessage req, TraceWriter log) 36 | { 37 | var pathValue = req.GetQueryNameValuePairs() 38 | .FirstOrDefault(q => string.Compare(q.Key, "file", true) == 0) 39 | .Value; 40 | 41 | var path = pathValue ?? ""; 42 | 43 | var staticFilesPath = Path.GetFullPath(Path.Combine(GetScriptPath(), staticFilesFolder)); 44 | var fullPath = Path.GetFullPath(Path.Combine(staticFilesPath, path)); 45 | 46 | if (!IsInDirectory(staticFilesPath, fullPath)) 47 | { 48 | throw new ArgumentException("Invalid path"); 49 | } 50 | 51 | var isDirectory = Directory.Exists(fullPath); 52 | if (isDirectory) 53 | { 54 | fullPath = Path.Combine(fullPath, defaultPage); 55 | } 56 | 57 | return fullPath; 58 | } 59 | 60 | private static bool IsInDirectory(string parentPath, string childPath) 61 | { 62 | var parent = new DirectoryInfo(parentPath); 63 | var child = new DirectoryInfo(childPath); 64 | 65 | var dir = child; 66 | do 67 | { 68 | if (dir.FullName == parent.FullName) 69 | { 70 | return true; 71 | } 72 | dir = dir.Parent; 73 | } while (dir != null); 74 | 75 | return false; 76 | } 77 | 78 | private static string GetMimeType(string filePath) 79 | { 80 | var fileInfo = new FileInfo(filePath); 81 | return MimeTypeMap.GetMimeType(fileInfo.Extension); 82 | } 83 | -------------------------------------------------------------------------------- /src/host.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /src/proxies.json: -------------------------------------------------------------------------------- 1 | { 2 | "proxies": { 3 | "files": { 4 | "matchCondition": { 5 | "route": "{*path}" 6 | }, 7 | "backendUri": "https://%WEBSITE_SITE_NAME%.azurewebsites.net/api/StaticFileServer?file={path}" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/www/assets/images/Azure-Functions-Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonychu/azure-functions-static-file-server/c0848d91b1d49e8e8f20d71327ca0026b669a447/src/www/assets/images/Azure-Functions-Logo.png -------------------------------------------------------------------------------- /src/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |Azure Functions Static File Server
16 |