├── .gitignore ├── AWSCLI.md ├── AwsCliTest.bat ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DONATIONS.md ├── Docker ├── compose-down.bat ├── compose-down.sh ├── compose-up.bat ├── compose-up.sh ├── compose.yaml ├── less3.db ├── run.bat ├── run.sh └── system.json ├── LICENSE.md ├── Less3.postman_collection ├── README.md ├── assets ├── CompatibilityTests.xlsx ├── diagram.png ├── diagram.pptx ├── favicon.ico ├── favicon.png ├── heart.ico ├── heart.png ├── heart.webp ├── icon.ico └── logo.png ├── cyberduck ├── Less3-pathstyle-http.cyberduckprofile └── Less3-virtualhosted.cyberduckprofile └── src ├── Less3.sln └── Less3 ├── Api ├── Admin │ ├── AdminApiHandler.cs │ ├── DeleteHandler.cs │ ├── GetHandler.cs │ └── PostHandler.cs └── S3 │ ├── ApiHandler.cs │ ├── ApiHelper.cs │ ├── BucketHandler.cs │ ├── ObjectHandler.cs │ └── ServiceHandler.cs ├── Assets ├── favicon.ico └── favicon.png ├── Classes ├── AuthManager.cs ├── AuthenticationResult.cs ├── AuthorizationResult.cs ├── Bucket.cs ├── BucketAcl.cs ├── BucketClient.cs ├── BucketManager.cs ├── BucketStatistics.cs ├── BucketTag.cs ├── ConfigManager.cs ├── ConsoleManager.cs ├── Constants.cs ├── Credential.cs ├── Obj.cs ├── ObjectAcl.cs ├── ObjectTag.cs ├── RequestMetadata.cs ├── RetentionType.cs ├── Settings.cs ├── Setup.cs └── User.cs ├── Common.cs ├── Dockerbuild.bat ├── Dockerfile ├── Dockerrun.bat ├── Dockerrun.sh ├── LICENSE.md ├── Less3.csproj ├── Less3.xml ├── Program.cs ├── Storage ├── DiskStorageDriver.cs ├── ObjectStream.cs ├── StorageDriver.cs └── StorageDriverType.cs ├── clean.bat ├── clean.sh └── heart.ico /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Visual Studio cache/options/bin directory 14 | .vs/ 15 | bin/ 16 | obj/ 17 | 18 | # MSTest test Results 19 | [Tt]est[Rr]esult*/ 20 | [Bb]uild[Ll]og.* 21 | 22 | # NUNIT 23 | *.VisualState.xml 24 | TestResult.xml 25 | 26 | # Build Results of an ATL Project 27 | [Dd]ebugPS/ 28 | [Rr]eleasePS/ 29 | dlldata.c 30 | 31 | # DNX 32 | project.lock.json 33 | project.fragment.lock.json 34 | artifacts/ 35 | 36 | *_i.c 37 | *_p.c 38 | *_i.h 39 | *.ilk 40 | *.meta 41 | *.obj 42 | *.pch 43 | *.pdb 44 | *.pgc 45 | *.pgd 46 | *.rsp 47 | *.sbr 48 | *.tlb 49 | *.tli 50 | *.tlh 51 | *.tmp 52 | *.tmp_proj 53 | *.log 54 | *.vspscc 55 | *.vssscc 56 | .builds 57 | *.pidb 58 | *.svclog 59 | *.scc 60 | 61 | # Chutzpah Test files 62 | _Chutzpah* 63 | 64 | # Visual C++ cache files 65 | ipch/ 66 | *.aps 67 | *.ncb 68 | *.opendb 69 | *.opensdf 70 | *.sdf 71 | *.cachefile 72 | *.VC.db 73 | *.VC.VC.opendb 74 | 75 | # Visual Studio profiler 76 | *.psess 77 | *.vsp 78 | *.vspx 79 | *.sap 80 | 81 | # TFS 2012 Local Workspace 82 | $tf/ 83 | 84 | # Guidance Automation Toolkit 85 | *.gpState 86 | 87 | # ReSharper is a .NET coding add-in 88 | _ReSharper*/ 89 | *.[Rr]e[Ss]harper 90 | *.DotSettings.user 91 | 92 | # JustCode is a .NET coding add-in 93 | .JustCode 94 | 95 | # TeamCity is a build add-in 96 | _TeamCity* 97 | 98 | # DotCover is a Code Coverage Tool 99 | *.dotCover 100 | 101 | # Visual Studio code coverage results 102 | *.coverage 103 | *.coveragexml 104 | 105 | # NCrunch 106 | _NCrunch_* 107 | .*crunch*.local.xml 108 | nCrunchTemp_* 109 | 110 | # MightyMoose 111 | *.mm.* 112 | AutoTest.Net/ 113 | 114 | # Web workbench (sass) 115 | .sass-cache/ 116 | 117 | # Installshield output folder 118 | [Ee]xpress/ 119 | 120 | # DocProject is a documentation generator add-in 121 | DocProject/buildhelp/ 122 | DocProject/Help/*.HxT 123 | DocProject/Help/*.HxC 124 | DocProject/Help/*.hhc 125 | DocProject/Help/*.hhk 126 | DocProject/Help/*.hhp 127 | DocProject/Help/Html2 128 | DocProject/Help/html 129 | 130 | # Click-Once directory 131 | publish/ 132 | 133 | # Publish Web Output 134 | *.[Pp]ublish.xml 135 | *.azurePubxml 136 | # TODO: Comment the next line if you want to checkin your web deploy settings 137 | # but database connection strings (with potential passwords) will be unencrypted 138 | *.pubxml 139 | *.publishproj 140 | 141 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 142 | # checkin your Azure Web App publish settings, but sensitive information contained 143 | # in these scripts will be unencrypted 144 | PublishScripts/ 145 | 146 | # NuGet Packages 147 | *.nupkg 148 | # The packages folder can be ignored because of Package Restore 149 | **/packages/* 150 | # except build/, which is used as an MSBuild target. 151 | !**/packages/build/ 152 | # Uncomment if necessary however generally it will be regenerated when needed 153 | #!**/packages/repositories.config 154 | # NuGet v3's project.json files produces more ignoreable files 155 | *.nuget.props 156 | *.nuget.targets 157 | 158 | # Microsoft Azure Build Output 159 | csx/ 160 | *.build.csdef 161 | 162 | # Microsoft Azure Emulator 163 | ecf/ 164 | rcf/ 165 | 166 | # Windows Store app package directories and files 167 | AppPackages/ 168 | BundleArtifacts/ 169 | Package.StoreAssociation.xml 170 | _pkginfo.txt 171 | 172 | # Visual Studio cache files 173 | # files ending in .cache can be ignored 174 | *.[Cc]ache 175 | # but keep track of directories ending in .cache 176 | !*.[Cc]ache/ 177 | 178 | # Others 179 | ClientBin/ 180 | ~$* 181 | *~ 182 | *.dbmdl 183 | *.dbproj.schemaview 184 | *.jfm 185 | *.pfx 186 | *.publishsettings 187 | node_modules/ 188 | orleans.codegen.cs 189 | 190 | # Since there are multiple workflows, uncomment next line to ignore bower_components 191 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 192 | #bower_components/ 193 | 194 | # RIA/Silverlight projects 195 | Generated_Code/ 196 | 197 | # Backup & report files from converting an old project file 198 | # to a newer Visual Studio version. Backup files are not needed, 199 | # because we have git ;-) 200 | _UpgradeReport_Files/ 201 | Backup*/ 202 | UpgradeLog*.XML 203 | UpgradeLog*.htm 204 | 205 | # SQL Server files 206 | *.mdf 207 | *.ldf 208 | 209 | # Business Intelligence projects 210 | *.rdl.data 211 | *.bim.layout 212 | *.bim_*.settings 213 | 214 | # Microsoft Fakes 215 | FakesAssemblies/ 216 | 217 | # GhostDoc plugin setting file 218 | *.GhostDoc.xml 219 | 220 | # Node.js Tools for Visual Studio 221 | .ntvs_analysis.dat 222 | 223 | # Visual Studio 6 build log 224 | *.plg 225 | 226 | # Visual Studio 6 workspace options file 227 | *.opt 228 | 229 | # Visual Studio LightSwitch build output 230 | **/*.HTMLClient/GeneratedArtifacts 231 | **/*.DesktopClient/GeneratedArtifacts 232 | **/*.DesktopClient/ModelManifest.xml 233 | **/*.Server/GeneratedArtifacts 234 | **/*.Server/ModelManifest.xml 235 | _Pvt_Extensions 236 | 237 | # Paket dependency manager 238 | .paket/paket.exe 239 | paket-files/ 240 | 241 | # FAKE - F# Make 242 | .fake/ 243 | 244 | # JetBrains Rider 245 | .idea/ 246 | *.sln.iml 247 | 248 | # CodeRush 249 | .cr/ 250 | 251 | # Python Tools for Visual Studio (PTVS) 252 | __pycache__/ 253 | *.pyc 254 | 255 | # Cake - Uncomment if you are using it 256 | # tools/ 257 | 258 | -------------------------------------------------------------------------------- /AWSCLI.md: -------------------------------------------------------------------------------- 1 | # Testing with AWS CLI 2 | 3 | Use the following commands to test a fresh installation of Less3 with the AWS CLI. 4 | 5 | Important: if you encounter any issues, re-submit the AWS CLI command using the ```--debug``` option, and please provide that output when filing an issue. 6 | 7 | ## Install Less3 8 | ``` 9 | > less3 10 | 11 | 12 | _ ____ 13 | | |___ _____|__ / 14 | | / -_|_-<_-<|_ \ 15 | |_\___/__/__/___/ 16 | 17 | 18 | 19 | 20 | <3 :: Less3 :: S3-Compatible Object Storage 21 | 22 | Thank you for using Less3! We're putting together a basic system configuration 23 | so you can be up and running quickly. You'll want to modify the system.json 24 | file after to ensure a more secure operating environment. 25 | 26 | 27 | Less3 requires access to a database and supports Sqlite, Microsoft SQL Server, 28 | MySQL, and PostgreSQL. Please provide access details for your database. The 29 | user account supplied must have the ability to CREATE and DROP tables along 30 | with issue queries containing SELECT, INSERT, UPDATE, and DELETE. Setup will 31 | attempt to create tables on your behalf if they dont exist. 32 | 33 | Database type [sqlite|sqlserver|mysql|postgresql]: [sqlite] 34 | Filename: [./less3.db] 35 | 36 | IMPORTANT: Using Sqlite in production is not recommended if deploying within a 37 | containerized environment and the database file is stored within the container. 38 | Store the database file in external storage to ensure persistence. 39 | 40 | 41 | All finished! 42 | 43 | If you ever want to return to this setup wizard, just re-run the application 44 | from the terminal with the 'setup' argument. 45 | 46 | We created a bucket containing a few sample files for you so that you can see 47 | your node in action. Access these files in the 'default' bucket using the 48 | AWS SDK or your favorite S3 browser tool. 49 | 50 | http://localhost:8000/default/hello.html 51 | http://localhost:8000/default/hello.txt 52 | http://localhost:8000/default/hello.json 53 | 54 | Access key : default 55 | Secret key : default 56 | Bucket name : default (public read enabled!) 57 | S3 endpoint : http://localhost:8000 58 | 59 | IMPORTANT: be sure to supply a hostname in the system.json Server.DnsHostname 60 | field if you wish to allow access from other machines. Your node is currently 61 | only accessible via localhost. Do not use an IP address for this value. 62 | 63 | 64 | 65 | 66 | ,d88b.d88b, _ ____ 67 | 88888888888 | |___ _____|__ / 68 | `Y8888888Y' | / -_|_-<_-<|_ \ 69 | `Y888Y' |_\___/__/__/___/ 70 | `Y' 71 | 72 | 73 | 74 | Less3 | S3-Compatible Object Storage | v2.1.0.0 75 | 76 | WARNING: Less3 started on 'localhost' 77 | Less3 can only service requests from the local machine. If you wish to serve 78 | external requests, edit the system.json file and specify a DNS-resolvable 79 | hostname in the Server.DnsHostname field. 80 | 81 | | Initializing logging 82 | | Initializing database 83 | | Initializing configuration manager 84 | | Initializing bucket manager 85 | | Initializing authentication manager 86 | | Initializing API handler 87 | | Initializing admin API handler 88 | | Initializing console manager 89 | | Initializing S3 server interface 90 | | http://localhost:8000 91 | | Initializing S3 server APIs 92 | | No base domain specified 93 | | Requests must use path-style hosted URLs, i.e. [hostname]/[bucket]/[key] 94 | 95 | Command (? for help) > 96 | ``` 97 | 98 | ## Set Access Material 99 | ``` 100 | $ aws configure 101 | AWS Access Key ID [****************ault]: default 102 | AWS Secret Access Key [****************ault]: default 103 | Default region name [us-west-1]: us-west-1 104 | Default output format [None]: 105 | ``` 106 | 107 | ## List Buckets 108 | ``` 109 | $ aws --endpoint-url http://localhost:8000 s3 ls s3:// 110 | 2022-09-14 21:43:27 default 111 | ``` 112 | 113 | ## Check Bucket Existence 114 | ``` 115 | $ aws s3api head-bucket --endpoint http://localhost:8000 --bucket default 116 | (no output) 117 | ``` 118 | 119 | ## List Default Bucket 120 | ``` 121 | $ aws --endpoint-url http://localhost:8000 s3 ls s3://default 122 | 2022-09-14 21:43:27 1193 hello.html 123 | 2022-09-14 21:43:27 217 hello.txt 124 | 2022-09-14 21:43:27 152 hello.json 125 | ``` 126 | 127 | ## Download an Object 128 | ``` 129 | $ aws --endpoint-url http://localhost:8000 s3 cp s3://default/hello.json ./hello.json 130 | download: s3://default/hello.json to .\hello.json 131 | ``` 132 | 133 | ## Upload an Object 134 | 135 | Versioning is disabled by default on buckets. Re-uploading the object that was just downloaded would fail. Rename the object first. 136 | 137 | ``` 138 | $ aws --endpoint-url http://localhost:8000 s3 cp ./hello.foo s3://default/hello.foo 139 | upload: .\hello.foo to s3://default/hello.foo 140 | ``` 141 | 142 | ## Check Object Existence 143 | ``` 144 | $ aws s3api head-object --endpoint http://localhost:8000 --bucket default --key hello.txt 145 | { 146 | "LastModified": "2022-09-14T21:43:27", 147 | "ContentLength": 217, 148 | "ETag": "626A3F7A01F364E917A5088E4856CADD", 149 | "ContentType": "application/octet-stream", 150 | "Metadata": {}, 151 | "StorageClass": "STANDARD" 152 | } 153 | ``` 154 | 155 | ## Delete an Object 156 | ``` 157 | $ aws --endpoint-url http://localhost:8000 s3 rm s3://default/hello.foo 158 | delete: s3://default/hello.foo 159 | ``` 160 | 161 | ## Create a Bucket 162 | ``` 163 | $ aws --endpoint-url http://localhost:8000 s3 mb s3://bucket2 164 | make_bucket: bucket2 165 | ``` 166 | 167 | ## Remove a Bucket 168 | ``` 169 | $ aws --endpoint-url http://localhost:8000 s3 rb s3://bucket2 170 | remove_bucket: bucket2 171 | ``` 172 | -------------------------------------------------------------------------------- /AwsCliTest.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | aws configure 3 | 4 | echo Listing buckets... 5 | aws --endpoint-url http://localhost:8000 s3 ls s3:// 6 | 7 | echo Checking if bucket default exists... 8 | aws s3api head-bucket --endpoint http://localhost:8000 --bucket default 9 | 10 | echo Listing bucket default... 11 | aws --endpoint-url http://localhost:8000 s3 ls s3://default 12 | 13 | echo Downloading hello.json... 14 | aws --endpoint-url http://localhost:8000 s3 cp s3://default/hello.json ./hello.json 15 | 16 | echo Copying default.json to hello.foo... 17 | copy /Y hello.json hello.foo 18 | 19 | echo Uploading hello.foo... 20 | aws --endpoint-url http://localhost:8000 s3 cp ./hello.foo s3://default/hello.foo 21 | 22 | echo Checking if object hello.foo exists... 23 | aws s3api head-object --endpoint http://localhost:8000 --bucket default --key hello.foo 24 | 25 | echo Deleting hello.foo... 26 | aws --endpoint-url http://localhost:8000 s3 rm s3://default/hello.foo 27 | 28 | echo Creating bucket bucket2... 29 | aws --endpoint-url http://localhost:8000 s3 mb s3://bucket2 30 | 31 | echo Deleting bucket bucket2... 32 | aws --endpoint-url http://localhost:8000 s3 rb s3://bucket2 33 | 34 | echo Cleaning up temporary files... 35 | del /q hello.foo 36 | del /q hello.json 37 | @echo on 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Current Version 4 | 5 | v2.1.x 6 | 7 | - Dependency update and changes to improve compatibility with AWS CLI 8 | - Testing with key AWS CLI capabilities, see AWSCLI.md 9 | 10 | ## Previous Versions 11 | 12 | v2.0.0 13 | 14 | - Dependency updates, internal refactor 15 | 16 | v1.5.0 17 | 18 | - Breaking change; signatures no longer being validated 19 | - Dependency updates 20 | - Folder fixes 21 | - Owner information included in enumeration 22 | - Better alerts on startup about request requirements (virtual hosting vs path style URLs) 23 | 24 | v1.4.0 25 | 26 | - Minor refactor 27 | - Fixes to enumeration including folder support 28 | - Request signature authentication 29 | 30 | v1.3.0.1 31 | 32 | - Migrate database layer to ORM 33 | - Improved usability and console log messages 34 | - Simplification of objects 35 | - Centralized authentication and authorization 36 | - Virtualized storage layer to support new backend storage options 37 | - Updated Postman collection 38 | - Dockerfile for containerized deployments 39 | 40 | v1.2.0.2 41 | 42 | - Minor cleanup, version from assembly, dependency update, XML documentation, Postman collection 43 | 44 | v1.2.0 45 | 46 | - Support for bucket in hostname or bucket in URL 47 | - Dependency update 48 | 49 | v1.1.0 50 | 51 | - Dependency update with performance improvements, better async behavior 52 | - Better support for large objects using streams instead of memory-intensive byte arrays 53 | - Better support for chunked transfer-encoding 54 | - Bugfixes 55 | 56 | v1.0.x 57 | 58 | - Added bucket location API 59 | - Changed serializer to remove pretty print for Cyberduck compatibility (S3 Java SDK compatibility) 60 | - Added ACL APIs 61 | - Authentication header support for both v2 and v4 62 | - Chunked transfer support 63 | - Initial release; please see supported APIs below. 64 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 44 | 45 | For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing! 4 | 5 | The following is a set of guidelines for contributing to our project on Github. These are mostly guidelines, not rules. 6 | 7 | ## Code of Conduct 8 | 9 | This project and everyone participating in it is governed by the Code of Conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior to project moderators. 10 | 11 | ## Pull Requests 12 | 13 | Please follow these guidelines when submitting pull requests (PRs): 14 | 15 | - PRs should be manageable in size to make it easy for us to validate and integrate 16 | - Each PR should be contained to a single fix or a single feature 17 | - Describe the motivation for the PR 18 | - Describe a methodology to test and validate, if appropriate 19 | 20 | Please ensure that the code in your PR follows a style similar to that of the project. If you find a material discrepancy between the style followed by the project and a de facto standard style given the project language and framework, please let us know so we can amend and make our code more maintainable. 21 | 22 | ## Asking Questions 23 | 24 | Prior to asking questions, please review closed issues and wiki pages. If your question is not answered in either of those places, please feel free to file an issue! This will also help us to build out documentation. 25 | 26 | ## Reporting Bugs 27 | 28 | If you encounter an issue, please let us know! We kindly ask that you supply the following information with your bug report when you file an issue. Feel free to copy/paste from the below and use as a template. 29 | 30 | --- Bug Report --- 31 | 32 | Operating system and version: Windows 10 33 | Framework and runtime: .NET Core 2.0 34 | Issue encountered: The widget shifted left 35 | Expected behavior: The widget should have shifted right 36 | Steps to reproduce: Instantiate the widget and call .ShiftRight() 37 | Sample code encapsulating the problem: 38 | ``` 39 | Widget widget = new Widget(); 40 | widget.ShiftRight(); 41 | ``` 42 | Exception details: [insert exception output here] 43 | Stack trace: [if appropriate] 44 | 45 | --- End --- 46 | 47 | ## Suggesting Enhancements 48 | 49 | Should there be a way that you feel we could improve upon this project, please feel free to file an issue and use the template below to provide the necessary details to support the request. 50 | 51 | Some basic guidelines for suggesting enhancements: 52 | 53 | - Use a clear and descriptive title for the issue to identify the suggestion. 54 | - Provide a step-by-step description of the suggested enhancement in as many details as possible. 55 | - Provide specific examples to demonstrate the steps including copy/pasteable snippets where possible 56 | - Describe the current behavior and the behavior you would like to see 57 | - Describe the usefulness of the enhancement to yourself and potentially to others 58 | 59 | --- Enhancement Request --- 60 | 61 | Enhancement request title: Widgets should have a color attribute 62 | Use case: I want to specify what color a widget is 63 | Current behavior: Widgets don't have a color 64 | Requested behavior: Allow me to specify a widget's color 65 | Recommended implementation: Add a Color attribute to the Widget class 66 | Usefulness of the enhancement: All widgets have color, and everyone has to build their own implementation to set this 67 | 68 | --- End --- 69 | -------------------------------------------------------------------------------- /DONATIONS.md: -------------------------------------------------------------------------------- 1 | ## Donations 2 | 3 | If you're interested in financially supporting this work on other open source projects I manage, first of all, thank you! It brings me delight to know that this software has helped you in some way. Please find below address details for donations using 4 | 5 | ### Traditional 6 | 7 | | Method | Address | 8 | |--------|---------| 9 | | PayPal | @joelchristner - https://paypal.me/joelchristner?country.x=US&locale.x=en_US | 10 | | Venmo | @Joel-Christner - https://account.venmo.com/u/Joel-Christner | 11 | 12 | ### Cryptocurrency (Mainstream) 13 | 14 | | Method | Address | 15 | |----------|---------| 16 | | Bitcoin | 3HRgnvEWQBDdWDp75CFDsz3PirCYmftDwU | 17 | | Ethereum | 0xE064dC84270e17e2Ac34b2552461d672BdBC5e36 | 18 | 19 | ### Cryptocurrency (Altcoins) 20 | 21 | | Method | Address | 22 | |--------|---------| 23 | | XRP | Tag 1765608084 Address rw2ciyaNshpHe7bCHo4bRWq6pqqynnWKQg | 24 | | Shiba Inu SHIB | 0xdA58D4ba0d5823d80a0C42C69E139124B889c69a | 25 | | Algorand ALGO | FFKA23KC4BHEU5HM4OQHLEILVIYDLRXYK6WD6UI573JPUGHZR43JVHAF7A | 26 | | Stellar Lumens XML | Memo 2264929895 Address GDQP2KPQGKIHYJGXNUIYOMHARUARCA7DJT5FO2FFOOKY3B2WSQHG4W37 | 27 | | Dogecoin DOGE | DQ2dn4UifpYA8RyuNF1t112Y1XH5L42Rxv | 28 | | Cardano ADA | addr1vxrxyrv0phgr2xcl08dj0sfy425mhqehuu4dy99ja4nhtwqfsvgjk | 29 | 30 | If you have a coin you prefer to use, please let me know! 31 | -------------------------------------------------------------------------------- /Docker/compose-down.bat: -------------------------------------------------------------------------------- 1 | docker compose -f compose.yaml down 2 | -------------------------------------------------------------------------------- /Docker/compose-down.sh: -------------------------------------------------------------------------------- 1 | docker compose -f compose.yaml down 2 | -------------------------------------------------------------------------------- /Docker/compose-up.bat: -------------------------------------------------------------------------------- 1 | docker compose -f compose.yaml up 2 | -------------------------------------------------------------------------------- /Docker/compose-up.sh: -------------------------------------------------------------------------------- 1 | docker compose -f compose.yaml up & 2 | -------------------------------------------------------------------------------- /Docker/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | # 4 | # Less3 5 | # See https://hub.docker.com/r/jchristn/less3 6 | # and https://github.com/jchristn/less3 7 | # 8 | 9 | litegraph: 10 | container_name: 'less3' 11 | image: 'jchristn/less3:v2.1.11' 12 | network_mode: 'host' 13 | stdin_open: true 14 | tty: true 15 | volumes: 16 | - ./system.json:/app/system.json 17 | - ./less3.db:/app/less3.db 18 | - ./logs/:/app/logs/ 19 | - ./temp/:/app/temp/ 20 | - ./disk/:/app/disk/ 21 | healthcheck: 22 | test: curl --fail http://localhost:8000 23 | -------------------------------------------------------------------------------- /Docker/less3.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchristn/Less3/b973c87d9ffe0d1c988593c472d33924e74207d9/Docker/less3.db -------------------------------------------------------------------------------- /Docker/run.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | IF "%1" == "" GOTO :Usage 4 | 5 | if not exist system.json ( 6 | echo Configuration file system.json not found. 7 | exit /b 1 8 | ) 9 | 10 | REM Items that require persistence 11 | REM system.json 12 | REM less3.db 13 | REM logs/ 14 | REM temp/ 15 | REM disk/ 16 | 17 | REM Argument order matters! 18 | 19 | docker run ^ 20 | -p 8000:8000 ^ 21 | -t ^ 22 | -i ^ 23 | -e "TERM=xterm-256color" ^ 24 | -v .\system.json:/app/system.json ^ 25 | -v .\less3.db:/app/less3.db ^ 26 | -v .\logs\:/app/logs/ ^ 27 | -v .\temp\:/app/temp/ ^ 28 | -v .\disk\:/app/disk/ ^ 29 | jchristn/less3:%1 30 | 31 | GOTO :Done 32 | 33 | :Usage 34 | ECHO Provide one argument indicating the tag. 35 | ECHO Example: dockerrun.bat v2.1.11 36 | :Done 37 | @echo on 38 | -------------------------------------------------------------------------------- /Docker/run.sh: -------------------------------------------------------------------------------- 1 | if [ -z "${IMG_TAG}" ]; then 2 | IMG_TAG='v2.1.11' 3 | fi 4 | 5 | echo Using image tag $IMG_TAG 6 | 7 | if [ ! -f "system.json" ] 8 | then 9 | echo Configuration file system.json not found. 10 | exit 11 | fi 12 | 13 | # Items that require persistence 14 | # system.json 15 | # less3.db 16 | # logs/ 17 | # temp/ 18 | # disk/ 19 | 20 | # Argument order matters! 21 | 22 | docker run \ 23 | -p 8000:8000 \ 24 | -t \ 25 | -i \ 26 | -e "TERM=xterm-256color" \ 27 | -v ./system.json:/app/system.json \ 28 | -v ./less3.db:/app/less3.db \ 29 | -v ./logs/:/app/logs/ \ 30 | -v ./temp/:/app/temp/ \ 31 | -v ./disk/:/app/disk/ \ 32 | jchristn/less3:$IMG_TAG 33 | 34 | -------------------------------------------------------------------------------- /Docker/system.json: -------------------------------------------------------------------------------- 1 | { 2 | "EnableConsole": true, 3 | "ValidateSignatures": true, 4 | "HeaderApiKey": "x-api-key", 5 | "AdminApiKey": "less3admin", 6 | "RegionString": "us-west-1", 7 | "Database": { 8 | "Filename": "./less3.db", 9 | "Type": "Sqlite", 10 | "Port": 0, 11 | "Debug": { 12 | "EnableForQueries": false, 13 | "EnableForResults": false 14 | } 15 | }, 16 | "Webserver": { 17 | "Hostname": "*", 18 | "Port": 8000, 19 | "IO": { 20 | "StreamBufferSize": 65536, 21 | "MaxRequests": 1024, 22 | "ReadTimeoutMs": 10000, 23 | "MaxIncomingHeadersSize": 65536, 24 | "EnableKeepAlive": false 25 | }, 26 | "Ssl": { 27 | "Enable": false, 28 | "MutuallyAuthenticate": false, 29 | "AcceptInvalidAcertificates": true 30 | }, 31 | "Headers": { 32 | "IncludeContentLength": true, 33 | "DefaultHeaders": { 34 | "Access-Control-Allow-Origin": "*", 35 | "Access-Control-Allow-Methods": "OPTIONS, HEAD, GET, PUT, POST, DELETE, PATCH", 36 | "Access-Control-Allow-Headers": "*", 37 | "Access-Control-Expose-Headers": "", 38 | "Accept": "*/*", 39 | "Accept-Language": "en-US, en", 40 | "Accept-Charset": "ISO-8859-1, utf-8", 41 | "Cache-Control": "no-cache", 42 | "Connection": "close", 43 | "Host": "localhost:8000" 44 | } 45 | }, 46 | "AccessControl": { 47 | "DenyList": {}, 48 | "PermitList": {}, 49 | "Mode": "DefaultPermit" 50 | }, 51 | "Debug": { 52 | "AccessControl": false, 53 | "Routing": false, 54 | "Requests": false, 55 | "Responses": false 56 | } 57 | }, 58 | "Storage": { 59 | "TempDirectory": "./temp/", 60 | "StorageType": "Disk", 61 | "DiskDirectory": "./disk/" 62 | }, 63 | "Logging": { 64 | "SyslogServerIp": "127.0.0.1", 65 | "SyslogServerPort": 514, 66 | "MinimumLevel": "Info", 67 | "LogHttpRequests": false, 68 | "LogS3Requests": false, 69 | "LogExceptions": false, 70 | "LogSignatureValidation": false, 71 | "ConsoleLogging": true, 72 | "DiskLogging": true, 73 | "DiskDirectory": "./logs/" 74 | }, 75 | "Debug": { 76 | "Authentication": false, 77 | "S3Requests": false, 78 | "Exceptions": false 79 | } 80 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![alt tag](https://github.com/jchristn/less3/blob/master/assets/logo.png) 2 | 3 | # Less3 :: S3-Compatible Object Storage 4 | 5 | Less3 is an S3-compatible object storage platform that you can run anywhere. 6 | 7 | ![alt tag](https://github.com/jchristn/less3/blob/master/assets/diagram.png) 8 | 9 | ## Use Cases 10 | 11 | Core use cases for Less3: 12 | 13 | - Local object storage - S3-compatible storage on your laptop, virtual machine, container, or bare metal 14 | - Private cloud object storage - use your existing private cloud hardware to create an S3-compatible storage pool 15 | - Development and test - local devtest against S3-compatible storage 16 | - Remote storage - deploy S3-compatible storage in environments where you must control data placement 17 | 18 | ## New in This Version 19 | 20 | v2.1.x 21 | 22 | - Dependency update and changes to improve compatibility with AWS CLI 23 | - Testing with key AWS CLI capabilities, see AWSCLI.md 24 | 25 | ## Help and Feedback 26 | 27 | First things first - do you need help or have feedback? Please file an issue here. 28 | 29 | ## Special Thanks 30 | 31 | Thanks to @iain-cyborn for helping make the platform better! 32 | 33 | ## Initial Setup 34 | 35 | The binaries for Less3 can be created by compiling from source. Executing the binary will create a system configuration in the ```system.json``` file along with the configuration database ```less3.db```. 36 | 37 | The ```Server.DnsHostname``` MUST be set to a hostname. You cannot use IP addresses (parsing will fail). Incoming HTTP requests must have a HOST header value that matches the value in ```Server.DnsHostname```. If it does not match, you will receive a ```400/Bad Request```. 38 | 39 | If you use ```*```, ```+```, or ```0.0.0.0``` for the ```Server.DnsHostname```, Less3 must be executed using administrative privileges (this is required by the underlying operating system). 40 | 41 | To get started, clone Less3, build, publish, and run! 42 | 43 | ``` 44 | $ git clone https://github.com/jchristn/less3 45 | $ cd less3 46 | $ dotnet build -f netcoreapp2.2 47 | $ dotnet publish -f netcoreapp2.2 48 | $ cd less3/bin/debug/netcoreapp2.2/publish 49 | $ dotnet less3.dll 50 | ``` 51 | 52 | ## S3 Client Compatibility 53 | 54 | Less3 was designed to be consumed using either the AWS SDK or direct RESTful integration in accordance with Amazon's official documentation (https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html). Should you encounter a discrepancy between how Less3 operates and how AWS S3 operates, please file an issue. 55 | 56 | I tested Less3 using the AWS SDK for C#, a live account on S3, CloudBerry Explorer for S3 (see https://www.cloudberrylab.com/explorer/windows/amazon-s3.aspx), and S3 Browser (see http://s3browser.com/). If you have or recommend other tools, please file an issue here and let me know! 57 | 58 | ## Supported APIs 59 | 60 | Please refer to the compatibility matrix found in 'assets' for a full list of supported APIs and caveats. 61 | 62 | The following APIs are supported with Less3: 63 | 64 | - Service APIs 65 | - ListBuckets 66 | 67 | - Bucket APIs 68 | - Write 69 | - WriteAcl 70 | - WriteTagging 71 | - WriteVersioning (no MFA delete support) 72 | - Delete 73 | - DeleteTagging 74 | - Exists 75 | - Read (list objects v2) 76 | - ReadAcl 77 | - ReadVersions 78 | - ReadTagging 79 | 80 | - Object APIs 81 | - Write 82 | - WriteAcl 83 | - WriteTagging 84 | - Delete 85 | - DeleteMultiple 86 | - DeleteTagging 87 | - Exists 88 | - Read 89 | - ReadAcl 90 | - ReadRange 91 | - ReadTagging 92 | 93 | ## API Support 94 | 95 | There are several minor differences between how S3 and less3 handle certain aspects of API requests. However, these should be inconsequential from the perspective of the developer (for instance, version IDs are numbers internally within less3 rather than strings). 96 | 97 | Should you find any incompatibilities or behavioral issues with the APIs listed above that are considered 'supported', please file an issue here along with details on the expected behavior. I've tried to mimic the behavior of S3 while building out the API logic. A link to the supporting documentation will also be helpful to aid me in righting the wrong :) 98 | 99 | ## Bucket in Hostname vs URL 100 | 101 | Less3 supports cases where having the bucket name as: 102 | - **Path style URLs** - the bucket name is part of the URL (```http://[hostname]/[bucket]/[key]```) 103 | - **Virtual hosted URLs** - the bucket name is part of the hostname (```http://[bucket].[hostname]/[key]```) 104 | 105 | To use **path style URLs** do **not** set ```Server.BaseDomain```. This is the default configuration. 106 | 107 | To use **virtual hosted URLs**, you must: 108 | 109 | - Set ```Server.BaseDomain``` - if your hostname is ```localhost```, set this value to ```.localhost``` (prepend with a period) 110 | - Set ```Server.DnsHostname``` to ```*``` 111 | - Run Less3 as administrator 112 | - Ensure your hosted hostnames (i.e. ```[bucket].[hostname]```) are resolvable through DNS to your machine 113 | 114 | ## Administrative APIs 115 | 116 | Please refer to the 'wiki' for helpful notes including how to use the administrative APIs. 117 | 118 | ## Open Source Packages 119 | 120 | Less3 is built using a series of open-source packages, including: 121 | 122 | - AWS SDK - https://github.com/aws/aws-sdk-net 123 | - S3 Server - https://github.com/jchristn/s3server 124 | - Watson Webserver - https://github.com/jchristn/WatsonWebserver 125 | - WatsonORM - https://github.com/jchristn/watsonorm 126 | 127 | ## Deployment in Docker 128 | 129 | Less3 is available in [DockerHub](https://hub.docker.com/r/jchristn/less3). Refer to the `Docker` directory for the `compose.yaml`, `system.json`, and `less3.db` files that you will need. By default, the node will listen on TCP port `8000` and you can use the access key `default` and secret key `default`. Be sure to connect without SSL, and use path-style URLs. 130 | 131 | ## Version History 132 | 133 | Refer to CHANGELOG.md for details. 134 | -------------------------------------------------------------------------------- /assets/CompatibilityTests.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchristn/Less3/b973c87d9ffe0d1c988593c472d33924e74207d9/assets/CompatibilityTests.xlsx -------------------------------------------------------------------------------- /assets/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchristn/Less3/b973c87d9ffe0d1c988593c472d33924e74207d9/assets/diagram.png -------------------------------------------------------------------------------- /assets/diagram.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchristn/Less3/b973c87d9ffe0d1c988593c472d33924e74207d9/assets/diagram.pptx -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchristn/Less3/b973c87d9ffe0d1c988593c472d33924e74207d9/assets/favicon.ico -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchristn/Less3/b973c87d9ffe0d1c988593c472d33924e74207d9/assets/favicon.png -------------------------------------------------------------------------------- /assets/heart.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchristn/Less3/b973c87d9ffe0d1c988593c472d33924e74207d9/assets/heart.ico -------------------------------------------------------------------------------- /assets/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchristn/Less3/b973c87d9ffe0d1c988593c472d33924e74207d9/assets/heart.png -------------------------------------------------------------------------------- /assets/heart.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchristn/Less3/b973c87d9ffe0d1c988593c472d33924e74207d9/assets/heart.webp -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchristn/Less3/b973c87d9ffe0d1c988593c472d33924e74207d9/assets/icon.ico -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchristn/Less3/b973c87d9ffe0d1c988593c472d33924e74207d9/assets/logo.png -------------------------------------------------------------------------------- /cyberduck/Less3-pathstyle-http.cyberduckprofile: -------------------------------------------------------------------------------- 1 | 2 | 3 | Protocol 4 | s3 5 | Vendor 6 | s3-path-style 7 | Scheme 8 | http 9 | Description 10 | S3 (Deprecated path style requests) 11 | Hostname Configurable 12 | 13 | Port Configurable 14 | 15 | Username Configurable 16 | 17 | Properties 18 | 19 | s3.bucket.virtualhost.disable=true 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /cyberduck/Less3-virtualhosted.cyberduckprofile: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Protocol 6 | s3 7 | Vendor 8 | s3-http 9 | Scheme 10 | http 11 | Description 12 | Less3 13 | Default Port 14 | 8000 15 | Hostname Configurable 16 | 17 | Port Configurable 18 | 19 | Username Configurable 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Less3.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.168 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Less3", "Less3\Less3.csproj", "{A6AF80E6-A92A-4A52-8ECD-713EFD3465DA}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {A6AF80E6-A92A-4A52-8ECD-713EFD3465DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {A6AF80E6-A92A-4A52-8ECD-713EFD3465DA}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {A6AF80E6-A92A-4A52-8ECD-713EFD3465DA}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {A6AF80E6-A92A-4A52-8ECD-713EFD3465DA}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {A039530E-9907-434C-8B92-D1CE3B972E9B} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/Less3/Api/Admin/AdminApiHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Api.Admin 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net.Http; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | using S3ServerLibrary; 11 | using SyslogLogging; 12 | using WatsonWebserver; 13 | using WatsonWebserver.Core; 14 | 15 | using Less3.Classes; 16 | 17 | /// 18 | /// Admin API handler. 19 | /// 20 | public class AdminApiHandler 21 | { 22 | #region Public-Members 23 | 24 | #endregion 25 | 26 | #region Private-Members 27 | 28 | private Settings _Settings; 29 | private LoggingModule _Logging; 30 | private ConfigManager _Config; 31 | private BucketManager _Buckets; 32 | private AuthManager _Auth; 33 | 34 | private GetHandler _GetHandler; 35 | private PostHandler _PostHandler; 36 | private DeleteHandler _DeleteHandler; 37 | 38 | #endregion 39 | 40 | #region Constructors-and-Factories 41 | 42 | internal AdminApiHandler( 43 | Settings settings, 44 | LoggingModule logging, 45 | ConfigManager config, 46 | BucketManager buckets, 47 | AuthManager auth) 48 | { 49 | if (settings == null) throw new ArgumentNullException(nameof(settings)); 50 | if (logging == null) throw new ArgumentNullException(nameof(logging)); 51 | if (config == null) throw new ArgumentNullException(nameof(config)); 52 | if (buckets == null) throw new ArgumentNullException(nameof(buckets)); 53 | if (auth == null) throw new ArgumentNullException(nameof(auth)); 54 | 55 | _Settings = settings; 56 | _Logging = logging; 57 | _Config = config; 58 | _Buckets = buckets; 59 | _Auth = auth; 60 | 61 | _GetHandler = new GetHandler(_Settings, _Logging, _Config, _Buckets, _Auth); 62 | _PostHandler = new PostHandler(_Settings, _Logging, _Config, _Buckets, _Auth); 63 | _DeleteHandler = new DeleteHandler(_Settings, _Logging, _Config, _Buckets, _Auth); 64 | } 65 | 66 | #endregion 67 | 68 | #region Internal-Methods 69 | 70 | internal async Task Process(S3Context ctx) 71 | { 72 | switch (ctx.Http.Request.Method) 73 | { 74 | case WatsonWebserver.Core.HttpMethod.GET: 75 | await _GetHandler.Process(ctx); 76 | return; 77 | case WatsonWebserver.Core.HttpMethod.POST: 78 | await _PostHandler.Process(ctx); 79 | return; 80 | case WatsonWebserver.Core.HttpMethod.DELETE: 81 | await _DeleteHandler.Process(ctx); 82 | return; 83 | } 84 | 85 | await ctx.Response.Send(S3ServerLibrary.S3Objects.ErrorCode.InvalidRequest); 86 | return; 87 | } 88 | 89 | #endregion 90 | 91 | #region Private-Methods 92 | 93 | #endregion 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Less3/Api/Admin/DeleteHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Api.Admin 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | using S3ServerLibrary; 11 | using SyslogLogging; 12 | 13 | using Less3.Classes; 14 | 15 | /// 16 | /// Admin API DELETE handler. 17 | /// 18 | public class DeleteHandler 19 | { 20 | #region Public-Members 21 | 22 | #endregion 23 | 24 | #region Private-Members 25 | 26 | private Settings _Settings; 27 | private LoggingModule _Logging; 28 | private ConfigManager _Config; 29 | private BucketManager _Buckets; 30 | private AuthManager _Auth; 31 | 32 | #endregion 33 | 34 | #region Constructors-and-Factories 35 | 36 | internal DeleteHandler( 37 | Settings settings, 38 | LoggingModule logging, 39 | ConfigManager config, 40 | BucketManager buckets, 41 | AuthManager auth) 42 | { 43 | if (settings == null) throw new ArgumentNullException(nameof(settings)); 44 | if (logging == null) throw new ArgumentNullException(nameof(logging)); 45 | if (config == null) throw new ArgumentNullException(nameof(config)); 46 | if (buckets == null) throw new ArgumentNullException(nameof(buckets)); 47 | if (auth == null) throw new ArgumentNullException(nameof(auth)); 48 | 49 | _Settings = settings; 50 | _Logging = logging; 51 | _Config = config; 52 | _Buckets = buckets; 53 | _Auth = auth; 54 | } 55 | 56 | #endregion 57 | 58 | #region Internal-Methods 59 | 60 | internal async Task Process(S3Context ctx) 61 | { 62 | if (ctx.Http.Request.Url.Elements[1].Equals("buckets")) 63 | { 64 | await DeleteBuckets(ctx); 65 | return; 66 | } 67 | else if (ctx.Http.Request.Url.Elements[1].Equals("users")) 68 | { 69 | await DeleteUsers(ctx); 70 | return; 71 | } 72 | else if (ctx.Http.Request.Url.Elements[1].Equals("credentials")) 73 | { 74 | await DeleteCredentials(ctx); 75 | return; 76 | } 77 | 78 | await ctx.Response.Send(S3ServerLibrary.S3Objects.ErrorCode.InvalidRequest); 79 | } 80 | 81 | #endregion 82 | 83 | #region Private-Methods 84 | 85 | private async Task DeleteBuckets(S3Context ctx) 86 | { 87 | if (ctx.Http.Request.Url.Elements.Length != 3) 88 | { 89 | await ctx.Response.Send(S3ServerLibrary.S3Objects.ErrorCode.InvalidRequest); 90 | return; 91 | } 92 | 93 | Bucket bucket = _Config.GetBucketByGuid(ctx.Http.Request.Url.Elements[2]); 94 | if (bucket == null) 95 | { 96 | ctx.Response.StatusCode = 404; 97 | ctx.Response.ContentType = "text/plain"; 98 | await ctx.Response.Send(); 99 | return; 100 | } 101 | 102 | bool destroy = false; 103 | if (ctx.Http.Request.Query.Elements.AllKeys.Contains("destroy")) destroy = true; 104 | _Buckets.Remove(bucket, destroy); 105 | 106 | ctx.Response.StatusCode = 204; 107 | ctx.Response.ContentType = "text/plain"; 108 | await ctx.Response.Send(); 109 | return; 110 | } 111 | 112 | private async Task DeleteUsers(S3Context ctx) 113 | { 114 | if (ctx.Http.Request.Url.Elements.Length != 3) 115 | { 116 | await ctx.Response.Send(S3ServerLibrary.S3Objects.ErrorCode.InvalidRequest); 117 | return; 118 | } 119 | 120 | User user = _Config.GetUserByGuid(ctx.Http.Request.Url.Elements[2]); 121 | if (user == null) 122 | { 123 | ctx.Response.StatusCode = 404; 124 | ctx.Response.ContentType = "text/plain"; 125 | await ctx.Response.Send(); 126 | return; 127 | } 128 | 129 | _Config.DeleteUser(user.GUID); 130 | 131 | ctx.Response.StatusCode = 204; 132 | ctx.Response.ContentType = "text/plain"; 133 | await ctx.Response.Send(); 134 | return; 135 | } 136 | 137 | private async Task DeleteCredentials(S3Context ctx) 138 | { 139 | if (ctx.Http.Request.Url.Elements.Length != 3) 140 | { 141 | await ctx.Response.Send(S3ServerLibrary.S3Objects.ErrorCode.InvalidRequest); 142 | return; 143 | } 144 | 145 | Credential cred = _Config.GetCredentialByGuid(ctx.Http.Request.Url.Elements[2]); 146 | if (cred == null) 147 | { 148 | ctx.Response.StatusCode = 404; 149 | ctx.Response.ContentType = "text/plain"; 150 | await ctx.Response.Send(); 151 | return; 152 | } 153 | 154 | _Config.DeleteCredential(cred.GUID); 155 | 156 | ctx.Response.StatusCode = 204; 157 | ctx.Response.ContentType = "text/plain"; 158 | await ctx.Response.Send(); 159 | return; 160 | } 161 | 162 | #endregion 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Less3/Api/Admin/GetHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Api.Admin 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | using S3ServerLibrary; 11 | using SyslogLogging; 12 | 13 | using Less3.Classes; 14 | 15 | /// 16 | /// Admin API GET handler. 17 | /// 18 | internal class GetHandler 19 | { 20 | #region Public-Members 21 | 22 | #endregion 23 | 24 | #region Private-Members 25 | 26 | private Settings _Settings; 27 | private LoggingModule _Logging; 28 | private ConfigManager _Config; 29 | private BucketManager _Buckets; 30 | private AuthManager _Auth; 31 | 32 | #endregion 33 | 34 | #region Constructors-and-Factories 35 | 36 | internal GetHandler( 37 | Settings settings, 38 | LoggingModule logging, 39 | ConfigManager config, 40 | BucketManager buckets, 41 | AuthManager auth) 42 | { 43 | if (settings == null) throw new ArgumentNullException(nameof(settings)); 44 | if (logging == null) throw new ArgumentNullException(nameof(logging)); 45 | if (config == null) throw new ArgumentNullException(nameof(config)); 46 | if (buckets == null) throw new ArgumentNullException(nameof(buckets)); 47 | if (auth == null) throw new ArgumentNullException(nameof(auth)); 48 | 49 | _Settings = settings; 50 | _Logging = logging; 51 | _Config = config; 52 | _Buckets = buckets; 53 | _Auth = auth; 54 | } 55 | 56 | #endregion 57 | 58 | #region Internal-Methods 59 | 60 | internal async Task Process(S3Context ctx) 61 | { 62 | if (ctx.Http.Request.Url.Elements[1].Equals("buckets")) 63 | { 64 | await GetBuckets(ctx); 65 | return; 66 | } 67 | else if (ctx.Http.Request.Url.Elements[1].Equals("users")) 68 | { 69 | await GetUsers(ctx); 70 | return; 71 | } 72 | else if (ctx.Http.Request.Url.Elements[1].Equals("credentials")) 73 | { 74 | await GetCredentials(ctx); 75 | return; 76 | } 77 | 78 | await ctx.Response.Send(S3ServerLibrary.S3Objects.ErrorCode.InvalidRequest); 79 | } 80 | 81 | #endregion 82 | 83 | #region Private-Methods 84 | 85 | private async Task GetBuckets(S3Context ctx) 86 | { 87 | if (ctx.Http.Request.Url.Elements.Length >= 3) 88 | { 89 | Bucket bucket = _Buckets.GetByGuid(ctx.Http.Request.Url.Elements[2]); 90 | if (bucket == null) 91 | { 92 | ctx.Response.StatusCode = 404; 93 | ctx.Response.ContentType = "text/plain"; 94 | await ctx.Response.Send(); 95 | return; 96 | } 97 | else 98 | { 99 | ctx.Response.StatusCode = 200; 100 | ctx.Response.ContentType = "application/json"; 101 | await ctx.Response.Send(SerializationHelper.SerializeJson(bucket, true)); 102 | return; 103 | } 104 | } 105 | else 106 | { 107 | List buckets = _Config.GetBuckets(); 108 | ctx.Response.StatusCode = 200; 109 | ctx.Response.ContentType = "application/json"; 110 | await ctx.Response.Send(SerializationHelper.SerializeJson(buckets, true)); 111 | return; 112 | } 113 | } 114 | 115 | private async Task GetUsers(S3Context ctx) 116 | { 117 | if (ctx.Http.Request.Url.Elements.Length >= 3) 118 | { 119 | User user = _Config.GetUserByGuid(ctx.Http.Request.Url.Elements[2]); 120 | if (user == null) 121 | { 122 | ctx.Response.StatusCode = 404; 123 | ctx.Response.ContentType = "text/plain"; 124 | await ctx.Response.Send(); 125 | return; 126 | } 127 | else 128 | { 129 | ctx.Response.StatusCode = 200; 130 | ctx.Response.ContentType = "application/json"; 131 | await ctx.Response.Send(SerializationHelper.SerializeJson(user, true)); 132 | return; 133 | } 134 | } 135 | else 136 | { 137 | List users = _Config.GetUsers(); 138 | ctx.Response.StatusCode = 200; 139 | ctx.Response.ContentType = "application/json"; 140 | await ctx.Response.Send(SerializationHelper.SerializeJson(users, true)); 141 | return; 142 | } 143 | } 144 | 145 | private async Task GetCredentials(S3Context ctx) 146 | { 147 | if (ctx.Http.Request.Url.Elements.Length >= 3) 148 | { 149 | Credential cred = _Config.GetCredentialByGuid(ctx.Http.Request.Url.Elements[2]); 150 | if (cred == null) 151 | { 152 | ctx.Response.StatusCode = 404; 153 | ctx.Response.ContentType = "text/plain"; 154 | await ctx.Response.Send(); 155 | return; 156 | } 157 | else 158 | { 159 | ctx.Response.StatusCode = 200; 160 | ctx.Response.ContentType = "application/json"; 161 | await ctx.Response.Send(SerializationHelper.SerializeJson(cred, true)); 162 | return; 163 | } 164 | } 165 | else 166 | { 167 | List creds = _Config.GetCredentials(); 168 | ctx.Response.StatusCode = 200; 169 | ctx.Response.ContentType = "application/json"; 170 | await ctx.Response.Send(SerializationHelper.SerializeJson(creds, true)); 171 | return; 172 | } 173 | } 174 | 175 | #endregion 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/Less3/Api/Admin/PostHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Api.Admin 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | using S3ServerLibrary; 11 | using SyslogLogging; 12 | 13 | using Less3.Classes; 14 | 15 | /// 16 | /// Admin API POST handler. 17 | /// 18 | internal class PostHandler 19 | { 20 | #region Public-Members 21 | 22 | #endregion 23 | 24 | #region Private-Members 25 | 26 | private Settings _Settings; 27 | private LoggingModule _Logging; 28 | private ConfigManager _Config; 29 | private BucketManager _Buckets; 30 | private AuthManager _Auth; 31 | 32 | #endregion 33 | 34 | #region Constructors-and-Factories 35 | 36 | internal PostHandler( 37 | Settings settings, 38 | LoggingModule logging, 39 | ConfigManager config, 40 | BucketManager buckets, 41 | AuthManager auth) 42 | { 43 | if (settings == null) throw new ArgumentNullException(nameof(settings)); 44 | if (logging == null) throw new ArgumentNullException(nameof(logging)); 45 | if (config == null) throw new ArgumentNullException(nameof(config)); 46 | if (buckets == null) throw new ArgumentNullException(nameof(buckets)); 47 | if (auth == null) throw new ArgumentNullException(nameof(auth)); 48 | 49 | _Settings = settings; 50 | _Logging = logging; 51 | _Config = config; 52 | _Buckets = buckets; 53 | _Auth = auth; 54 | } 55 | 56 | #endregion 57 | 58 | #region Internal-Methods 59 | 60 | internal async Task Process(S3Context ctx) 61 | { 62 | if (ctx.Http.Request.Url.Elements[1].Equals("buckets")) 63 | { 64 | await PostBuckets(ctx); 65 | return; 66 | } 67 | else if (ctx.Http.Request.Url.Elements[1].Equals("users")) 68 | { 69 | await PostUsers(ctx); 70 | return; 71 | } 72 | else if (ctx.Http.Request.Url.Elements[1].Equals("credentials")) 73 | { 74 | await PostCredentials(ctx); 75 | return; 76 | } 77 | 78 | await ctx.Response.Send(S3ServerLibrary.S3Objects.ErrorCode.InvalidRequest); 79 | } 80 | 81 | #endregion 82 | 83 | #region Private-Methods 84 | 85 | private async Task PostBuckets(S3Context ctx) 86 | { 87 | if (ctx.Http.Request.Url.Elements.Length != 2) 88 | { 89 | await ctx.Response.Send(S3ServerLibrary.S3Objects.ErrorCode.InvalidRequest); 90 | return; 91 | } 92 | 93 | byte[] data = null; 94 | Bucket bucket = null; 95 | 96 | try 97 | { 98 | data = Common.StreamToBytes(ctx.Request.Data); 99 | bucket = SerializationHelper.DeserializeJson(Encoding.UTF8.GetString(data)); 100 | } 101 | catch (Exception) 102 | { 103 | await ctx.Response.Send(S3ServerLibrary.S3Objects.ErrorCode.InvalidRequest); 104 | return; 105 | } 106 | 107 | Bucket tempBucket = _Config.GetBucketByName(bucket.Name); 108 | if (tempBucket != null) 109 | { 110 | await ctx.Response.Send(S3ServerLibrary.S3Objects.ErrorCode.BucketAlreadyExists); 111 | return; 112 | } 113 | 114 | _Buckets.Add(bucket); 115 | 116 | ctx.Response.StatusCode = 201; 117 | ctx.Response.ContentType = "text/plain"; 118 | await ctx.Response.Send(); 119 | } 120 | 121 | private async Task PostUsers(S3Context ctx) 122 | { 123 | if (ctx.Http.Request.Url.Elements.Length != 2) 124 | { 125 | await ctx.Response.Send(S3ServerLibrary.S3Objects.ErrorCode.InvalidRequest); 126 | return; 127 | } 128 | 129 | User user = null; 130 | 131 | try 132 | { 133 | user = SerializationHelper.DeserializeJson(ctx.Request.DataAsString); 134 | } 135 | catch (Exception) 136 | { 137 | await ctx.Response.Send(S3ServerLibrary.S3Objects.ErrorCode.InvalidRequest); 138 | return; 139 | } 140 | 141 | User tempUser = _Config.GetUserByEmail(user.Email); 142 | if (tempUser != null) 143 | { 144 | ctx.Response.StatusCode = 409; 145 | ctx.Response.ContentType = "text/plain"; 146 | await ctx.Response.Send(); 147 | return; 148 | } 149 | 150 | tempUser = _Config.GetUserByGuid(user.GUID); 151 | if (tempUser != null) 152 | { 153 | ctx.Response.StatusCode = 409; 154 | ctx.Response.ContentType = "text/plain"; 155 | await ctx.Response.Send(); 156 | return; 157 | } 158 | 159 | _Config.AddUser(user); 160 | 161 | ctx.Response.StatusCode = 201; 162 | ctx.Response.ContentType = "text/plain"; 163 | await ctx.Response.Send(); 164 | } 165 | 166 | private async Task PostCredentials(S3Context ctx) 167 | { 168 | if (ctx.Http.Request.Url.Elements.Length != 2) 169 | { 170 | await ctx.Response.Send(S3ServerLibrary.S3Objects.ErrorCode.InvalidRequest); 171 | return; 172 | } 173 | 174 | byte[] data = null; 175 | Credential cred = null; 176 | 177 | try 178 | { 179 | data = Common.StreamToBytes(ctx.Request.Data); 180 | cred = SerializationHelper.DeserializeJson(Encoding.UTF8.GetString(data)); 181 | } 182 | catch (Exception) 183 | { 184 | await ctx.Response.Send(S3ServerLibrary.S3Objects.ErrorCode.InvalidRequest); 185 | return; 186 | } 187 | 188 | Credential tempCred = _Config.GetCredentialByAccessKey(cred.AccessKey); 189 | if (tempCred != null) 190 | { 191 | ctx.Response.StatusCode = 409; 192 | ctx.Response.ContentType = "text/plain"; 193 | await ctx.Response.Send(); 194 | return; 195 | } 196 | 197 | _Config.AddCredential(cred); 198 | 199 | ctx.Response.StatusCode = 201; 200 | ctx.Response.ContentType = "text/plain"; 201 | await ctx.Response.Send(); 202 | } 203 | 204 | #endregion 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/Less3/Api/S3/ApiHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Api.S3 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | using S3ServerLibrary; 10 | using S3ServerLibrary.S3Objects; 11 | 12 | using SyslogLogging; 13 | 14 | using Less3.Classes; 15 | 16 | /// 17 | /// API handler. 18 | /// 19 | internal class ApiHandler 20 | { 21 | #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously 22 | 23 | #region Public-Members 24 | 25 | #endregion 26 | 27 | #region Private-Members 28 | 29 | private Settings _Settings; 30 | private LoggingModule _Logging; 31 | private ConfigManager _Config; 32 | private BucketManager _Buckets; 33 | private AuthManager _Auth; 34 | 35 | private ServiceHandler _ServiceHandler; 36 | private BucketHandler _BucketHandler; 37 | private ObjectHandler _ObjectHandler; 38 | 39 | #endregion 40 | 41 | #region Constructors-and-Factories 42 | 43 | internal ApiHandler( 44 | Settings settings, 45 | LoggingModule logging, 46 | ConfigManager config, 47 | BucketManager buckets, 48 | AuthManager auth) 49 | { 50 | if (settings == null) throw new ArgumentNullException(nameof(settings)); 51 | if (logging == null) throw new ArgumentNullException(nameof(logging)); 52 | if (config == null) throw new ArgumentNullException(nameof(config)); 53 | if (buckets == null) throw new ArgumentNullException(nameof(buckets)); 54 | if (auth == null) throw new ArgumentNullException(nameof(auth)); 55 | 56 | _Settings = settings; 57 | _Logging = logging; 58 | _Config = config; 59 | _Buckets = buckets; 60 | _Auth = auth; 61 | 62 | _ServiceHandler = new ServiceHandler(_Settings, _Logging, _Config, _Buckets, _Auth); 63 | _BucketHandler = new BucketHandler(_Settings, _Logging, _Config, _Buckets, _Auth); 64 | _ObjectHandler = new ObjectHandler(_Settings, _Logging, _Config, _Buckets, _Auth); 65 | } 66 | 67 | #endregion 68 | 69 | #region Internal-Methods 70 | 71 | #region Service-APIs 72 | 73 | internal async Task ServiceListBuckets(S3Context ctx) 74 | { 75 | return await _ServiceHandler.ListBuckets(ctx); 76 | } 77 | 78 | internal async Task ServiceExists(S3Context ctx) 79 | { 80 | return _Settings.RegionString; 81 | } 82 | 83 | internal string FindMatchingBaseDomain(string hostname) 84 | { 85 | if (String.IsNullOrEmpty(hostname)) return null; 86 | if (String.IsNullOrEmpty(_Settings.BaseDomain)) return null; 87 | 88 | if (hostname.Equals(_Settings.BaseDomain)) return _Settings.BaseDomain; 89 | 90 | string testDomain = "." + _Settings.BaseDomain; 91 | if (hostname.EndsWith(testDomain)) return _Settings.BaseDomain; 92 | 93 | throw new KeyNotFoundException("A base domain could not be found for hostname '" + hostname + "'."); 94 | } 95 | 96 | #endregion 97 | 98 | #region Bucket-APIs 99 | 100 | internal async Task BucketDelete(S3Context ctx) 101 | { 102 | await _BucketHandler.Delete(ctx); 103 | } 104 | 105 | internal async Task BucketDeleteTagging(S3Context ctx) 106 | { 107 | await _BucketHandler.DeleteTags(ctx); 108 | } 109 | 110 | internal async Task BucketExists(S3Context ctx) 111 | { 112 | return await _BucketHandler.Exists(ctx); 113 | } 114 | 115 | internal async Task BucketReadLocation(S3Context ctx) 116 | { 117 | return await _BucketHandler.ReadLocation(ctx); 118 | } 119 | 120 | internal async Task BucketRead(S3Context ctx) 121 | { 122 | return await _BucketHandler.Read(ctx); 123 | } 124 | 125 | internal async Task BucketReadAcl(S3Context ctx) 126 | { 127 | return await _BucketHandler.ReadAcl(ctx); 128 | } 129 | 130 | internal async Task BucketReadTagging(S3Context ctx) 131 | { 132 | return await _BucketHandler.ReadTags(ctx); 133 | } 134 | 135 | internal async Task BucketReadVersions(S3Context ctx) 136 | { 137 | return await _BucketHandler.ReadVersions(ctx); 138 | } 139 | 140 | internal async Task BucketReadVersioning(S3Context ctx) 141 | { 142 | return await _BucketHandler.ReadVersioning(ctx); 143 | } 144 | 145 | internal async Task BucketWrite(S3Context ctx) 146 | { 147 | await _BucketHandler.Write(ctx); 148 | } 149 | 150 | internal async Task BucketWriteAcl(S3Context ctx, AccessControlPolicy acp) 151 | { 152 | await _BucketHandler.WriteAcl(ctx, acp); 153 | } 154 | 155 | internal async Task BucketWriteTagging(S3Context ctx, Tagging tagging) 156 | { 157 | await _BucketHandler.WriteTagging(ctx, tagging); 158 | } 159 | 160 | internal async Task BucketWriteVersioning(S3Context ctx, VersioningConfiguration versioning) 161 | { 162 | await _BucketHandler.WriteVersioning(ctx, versioning); 163 | } 164 | 165 | #endregion 166 | 167 | #region Object-APIs 168 | 169 | internal async Task ObjectDelete(S3Context ctx) 170 | { 171 | await _ObjectHandler.Delete(ctx); 172 | } 173 | 174 | internal async Task ObjectDeleteMultiple(S3Context ctx, DeleteMultiple dm) 175 | { 176 | return await _ObjectHandler.DeleteMultiple(ctx, dm); 177 | } 178 | 179 | internal async Task ObjectDeleteTagging(S3Context ctx) 180 | { 181 | await _ObjectHandler.DeleteTags(ctx); 182 | } 183 | 184 | internal async Task ObjectExists(S3Context ctx) 185 | { 186 | return await _ObjectHandler.Exists(ctx); 187 | } 188 | 189 | internal async Task ObjectRead(S3Context ctx) 190 | { 191 | return await _ObjectHandler.Read(ctx); 192 | } 193 | 194 | internal async Task ObjectReadAcl(S3Context ctx) 195 | { 196 | return await _ObjectHandler.ReadAcl(ctx); 197 | } 198 | 199 | internal async Task ObjectReadRange(S3Context ctx) 200 | { 201 | return await _ObjectHandler.ReadRange(ctx); 202 | } 203 | 204 | internal async Task ObjectReadTagging(S3Context ctx) 205 | { 206 | return await _ObjectHandler.ReadTags(ctx); 207 | } 208 | 209 | internal async Task ObjectWrite(S3Context ctx) 210 | { 211 | await _ObjectHandler.Write(ctx); 212 | } 213 | 214 | internal async Task ObjectWriteAcl(S3Context ctx, AccessControlPolicy acp) 215 | { 216 | await _ObjectHandler.WriteAcl(ctx, acp); 217 | } 218 | 219 | internal async Task ObjectWriteTagging(S3Context ctx, Tagging tagging) 220 | { 221 | await _ObjectHandler.WriteTagging(ctx, tagging); 222 | } 223 | 224 | #endregion 225 | 226 | #endregion 227 | 228 | #region Private-Methods 229 | 230 | private string AmazonTimestamp(DateTime dt) 231 | { 232 | return dt.ToString("yyyy-MM-ddTHH:mm:ss.fffz"); 233 | } 234 | 235 | #endregion 236 | 237 | #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/Less3/Api/S3/ApiHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Api.S3 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using S3ServerLibrary; 8 | using Less3.Classes; 9 | 10 | internal static class ApiHelper 11 | { 12 | internal static RequestMetadata GetRequestMetadata(S3Context ctx) 13 | { 14 | if (ctx == null) return null; 15 | if (ctx.Metadata == null) return null; 16 | return (RequestMetadata)(ctx.Metadata); 17 | } 18 | 19 | internal static string AmazonTimestamp(DateTime dt) 20 | { 21 | return dt.ToString("yyyy-MM-ddTHH:mm:ss.fffz"); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Less3/Api/S3/ServiceHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Api.S3 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | using SyslogLogging; 10 | using S3ServerLibrary; 11 | using S3ServerLibrary.S3Objects; 12 | 13 | using Less3.Classes; 14 | 15 | /// 16 | /// Service APIs. 17 | /// 18 | public class ServiceHandler 19 | { 20 | #region Public-Members 21 | 22 | #endregion 23 | 24 | #region Private-Members 25 | 26 | private Settings _Settings = null; 27 | private LoggingModule _Logging = null; 28 | private ConfigManager _Config = null; 29 | private BucketManager _Buckets = null; 30 | private AuthManager _Auth = null; 31 | 32 | #endregion 33 | 34 | #region Constructors-and-Factories 35 | 36 | internal ServiceHandler( 37 | Settings settings, 38 | LoggingModule logging, 39 | ConfigManager config, 40 | BucketManager buckets, 41 | AuthManager auth) 42 | { 43 | if (settings == null) throw new ArgumentNullException(nameof(settings)); 44 | if (logging == null) throw new ArgumentNullException(nameof(logging)); 45 | if (config == null) throw new ArgumentNullException(nameof(config)); 46 | if (buckets == null) throw new ArgumentNullException(nameof(buckets)); 47 | if (auth == null) throw new ArgumentNullException(nameof(auth)); 48 | 49 | _Settings = settings; 50 | _Logging = logging; 51 | _Config = config; 52 | _Buckets = buckets; 53 | _Auth = auth; 54 | } 55 | 56 | #endregion 57 | 58 | #region Internal-Methods 59 | 60 | #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously 61 | internal async Task ListBuckets(S3Context ctx) 62 | #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously 63 | { 64 | string header = "[" + ctx.Http.Request.Source.IpAddress + ":" + ctx.Http.Request.Source.Port + " " + ctx.Request.RequestType.ToString() + "] "; 65 | 66 | RequestMetadata md = ApiHelper.GetRequestMetadata(ctx); 67 | if (md == null) 68 | { 69 | _Logging.Warn(header + "unable to retrieve metadata"); 70 | throw new S3Exception(new Error(ErrorCode.InternalError)); 71 | } 72 | 73 | if (md.Authentication != AuthenticationResult.Authenticated) 74 | { 75 | _Logging.Warn(header + "requestor not authenticated"); 76 | throw new S3Exception(new Error(ErrorCode.AccessDenied)); 77 | } 78 | else 79 | { 80 | md.Authorization = AuthorizationResult.PermitService; 81 | } 82 | 83 | List buckets = _Buckets.GetUserBuckets(md.User.GUID); 84 | 85 | ListAllMyBucketsResult listBucketsResult = new ListAllMyBucketsResult(); 86 | listBucketsResult.Owner = new S3ServerLibrary.S3Objects.Owner(); 87 | listBucketsResult.Owner.DisplayName = md.User.Name; 88 | listBucketsResult.Owner.ID = md.User.Name; 89 | 90 | listBucketsResult.Buckets = new Buckets(); 91 | listBucketsResult.Buckets.BucketList = new List(); 92 | 93 | foreach (Classes.Bucket curr in buckets) 94 | { 95 | S3ServerLibrary.S3Objects.Bucket b = new S3ServerLibrary.S3Objects.Bucket(); 96 | b.Name = curr.Name; 97 | b.CreationDate = curr.CreatedUtc; 98 | listBucketsResult.Buckets.BucketList.Add(b); 99 | } 100 | 101 | return listBucketsResult; 102 | } 103 | 104 | #endregion 105 | 106 | #region Private-Methods 107 | 108 | #endregion 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Less3/Assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchristn/Less3/b973c87d9ffe0d1c988593c472d33924e74207d9/src/Less3/Assets/favicon.ico -------------------------------------------------------------------------------- /src/Less3/Assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchristn/Less3/b973c87d9ffe0d1c988593c472d33924e74207d9/src/Less3/Assets/favicon.png -------------------------------------------------------------------------------- /src/Less3/Classes/AuthenticationResult.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Classes 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using System.Text.Json; 7 | using System.Text.Json.Serialization; 8 | using System.Runtime.Serialization; 9 | 10 | /// 11 | /// Authentication result. 12 | /// 13 | [JsonConverter(typeof(JsonStringEnumConverter))] 14 | public enum AuthenticationResult 15 | { 16 | /// 17 | /// No authentication material was supplied. 18 | /// 19 | [EnumMember(Value = "NoMaterialSupplied")] 20 | NoMaterialSupplied, 21 | /// 22 | /// The user was not found. 23 | /// 24 | [EnumMember(Value = "UserNotFound")] 25 | UserNotFound, 26 | /// 27 | /// The supplied access key was not found. 28 | /// 29 | [EnumMember(Value = "AccessKeyNotFound")] 30 | AccessKeyNotFound, 31 | /// 32 | /// Authentication was successful. 33 | /// 34 | [EnumMember(Value = "Authenticated")] 35 | Authenticated, 36 | /// 37 | /// Authentication was not successful. 38 | /// 39 | [EnumMember(Value = "NotAuthenticated")] 40 | NotAuthenticated 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Less3/Classes/AuthorizationResult.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Classes 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using System.Text.Json; 7 | using System.Text.Json.Serialization; 8 | using System.Runtime.Serialization; 9 | 10 | /// 11 | /// Authorization result. 12 | /// 13 | [JsonConverter(typeof(JsonStringEnumConverter))] 14 | public enum AuthorizationResult 15 | { 16 | /// 17 | /// Operation authorized due to admin privileges. 18 | /// 19 | [EnumMember(Value = "AdminAuthorized")] 20 | AdminAuthorized, 21 | /// 22 | /// Service operations are by default permitted for any authenticated user. 23 | /// 24 | [EnumMember(Value = "PermitService")] 25 | PermitService, 26 | /// 27 | /// Operation permitted due to bucket global configuration. 28 | /// 29 | [EnumMember(Value = "PermitBucketGlobalConfig")] 30 | PermitBucketGlobalConfig, 31 | /// 32 | /// Operation permitted due to bucket all users access control list. 33 | /// 34 | [EnumMember(Value = "PermitBucketAllUsersAcl")] 35 | PermitBucketAllUsersAcl, 36 | /// 37 | /// Operation permitted due to bucket authenticated users access control list. 38 | /// 39 | [EnumMember(Value = "PermitBucketAuthUserAcl")] 40 | PermitBucketAuthUserAcl, 41 | /// 42 | /// Operation permitted due to bucket user access control list. 43 | /// 44 | [EnumMember(Value = "PermitBucketUserAcl")] 45 | PermitBucketUserAcl, 46 | /// 47 | /// Operation permitted due to bucket ownership. 48 | /// 49 | [EnumMember(Value = "PermitBucketOwnership")] 50 | PermitBucketOwnership, 51 | /// 52 | /// Operation permitted due to object all users access control list. 53 | /// 54 | [EnumMember(Value = "PermitObjectAllUsersAcl")] 55 | PermitObjectAllUsersAcl, 56 | /// 57 | /// Operation permitted due to object authenticated users access control list. 58 | /// 59 | [EnumMember(Value = "PermitObjectAuthUserAcl")] 60 | PermitObjectAuthUserAcl, 61 | /// 62 | /// Operation permitted due to object user access control list. 63 | /// 64 | [EnumMember(Value = "PermitObjectUserAcl")] 65 | PermitObjectUserAcl, 66 | /// 67 | /// Operation permitted due to object ownership. 68 | /// 69 | [EnumMember(Value = "PermitObjectOwnership")] 70 | PermitObjectOwnership, 71 | /// 72 | /// Operation not authorized. 73 | /// 74 | [EnumMember(Value = "NotAuthorized")] 75 | NotAuthorized 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Less3/Classes/Bucket.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Classes 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Data; 6 | using System.IO; 7 | using System.Text; 8 | using System.Text.Json; 9 | using System.Text.Json.Serialization; 10 | using Watson.ORM.Core; 11 | using Less3.Storage; 12 | 13 | /// 14 | /// Bucket configuration. 15 | /// 16 | [Table("buckets")] 17 | public class Bucket 18 | { 19 | #region Public-Members 20 | 21 | /// 22 | /// Database identifier. 23 | /// 24 | [JsonIgnore] 25 | [Column("id", true, DataTypes.Int, false)] 26 | public int Id { get; set; } = 0; 27 | 28 | /// 29 | /// GUID of the bucket. 30 | /// 31 | [Column("guid", false, DataTypes.Nvarchar, 64, false)] 32 | public string GUID { get; set; } = Guid.NewGuid().ToString(); 33 | 34 | /// 35 | /// GUID of the owner. 36 | /// 37 | [Column("ownerguid", false, DataTypes.Nvarchar, 64, false)] 38 | public string OwnerGUID { get; set; } = Guid.NewGuid().ToString(); 39 | 40 | /// 41 | /// Name of the bucket. 42 | /// 43 | [Column("name", false, DataTypes.Nvarchar, 256, false)] 44 | public string Name { get; set; } = null; 45 | 46 | /// 47 | /// Bucket region string. 48 | /// 49 | [Column("regionstring", false, DataTypes.Nvarchar, 32, false)] 50 | public string RegionString { get; set; } = "us-west-1"; 51 | 52 | /// 53 | /// Type of storage driver. 54 | /// 55 | [Column("storagetype", false, DataTypes.Enum, 16, false)] 56 | public StorageDriverType StorageType { get; set; } = StorageDriverType.Disk; 57 | 58 | /// 59 | /// Objects directory. 60 | /// 61 | [Column("diskdirectory", false, DataTypes.Nvarchar, 256, false)] 62 | public string DiskDirectory { get; set; } = "./disk/"; 63 | 64 | /// 65 | /// Enable or disable versioning. 66 | /// 67 | [Column("enableversioning", false, DataTypes.Boolean, false)] 68 | public bool EnableVersioning { get; set; } = false; 69 | 70 | /// 71 | /// Enable or disable public write. 72 | /// 73 | [Column("enablepublicwrite", false, DataTypes.Boolean, false)] 74 | public bool EnablePublicWrite { get; set; } = false; 75 | 76 | /// 77 | /// Enable or disable public read. 78 | /// 79 | [Column("enablepublicread", false, DataTypes.Boolean, false)] 80 | public bool EnablePublicRead { get; set; } = false; 81 | 82 | /// 83 | /// Creation timestamp. 84 | /// 85 | [Column("createdutc", false, DataTypes.DateTime, false)] 86 | public DateTime CreatedUtc { get; set; } = DateTime.Now.ToUniversalTime(); 87 | 88 | #endregion 89 | 90 | #region Private-Members 91 | 92 | #endregion 93 | 94 | #region Constructors-and-Factories 95 | 96 | /// 97 | /// Instantiate. 98 | /// 99 | public Bucket() 100 | { 101 | 102 | } 103 | 104 | /// 105 | /// Instantiate. 106 | /// 107 | /// Name. 108 | /// Owner GUID. 109 | /// Storage type. 110 | /// Disk directory. 111 | /// Region. 112 | public Bucket( 113 | string name, 114 | string owner, 115 | StorageDriverType storageType, 116 | string diskDirectory, 117 | string region = "us-west-1") 118 | { 119 | if (String.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); 120 | if (String.IsNullOrEmpty(owner)) throw new ArgumentNullException(nameof(owner)); 121 | if (String.IsNullOrEmpty(diskDirectory)) throw new ArgumentNullException(nameof(diskDirectory)); 122 | 123 | Name = name; 124 | RegionString = region; 125 | StorageType = storageType; 126 | DiskDirectory = diskDirectory; 127 | OwnerGUID = owner; 128 | CreatedUtc = DateTime.Now.ToUniversalTime(); 129 | } 130 | 131 | /// 132 | /// Instantiate. 133 | /// 134 | /// GUID. 135 | /// Name. 136 | /// Owner GUID. 137 | /// Storage type. 138 | /// Disk directory. 139 | /// Region. 140 | public Bucket( 141 | string guid, 142 | string name, 143 | string owner, 144 | StorageDriverType storageType, 145 | string diskDirectory, 146 | string region = "us-west-1") 147 | { 148 | if (String.IsNullOrEmpty(guid)) throw new ArgumentNullException(nameof(guid)); 149 | if (String.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); 150 | if (String.IsNullOrEmpty(owner)) throw new ArgumentNullException(nameof(owner)); 151 | if (String.IsNullOrEmpty(diskDirectory)) throw new ArgumentNullException(nameof(diskDirectory)); 152 | 153 | GUID = guid; 154 | Name = name; 155 | RegionString = region; 156 | StorageType = storageType; 157 | DiskDirectory = diskDirectory; 158 | OwnerGUID = owner; 159 | CreatedUtc = DateTime.Now.ToUniversalTime(); 160 | } 161 | 162 | #endregion 163 | 164 | #region Public-Methods 165 | 166 | #endregion 167 | 168 | #region Private-Methods 169 | 170 | #endregion 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/Less3/Classes/BucketAcl.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Classes 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Data; 6 | using System.Text; 7 | using System.Text.Json; 8 | using System.Text.Json.Serialization; 9 | using Watson.ORM.Core; 10 | 11 | /// 12 | /// Access control list entry for a bucket. 13 | /// 14 | [Table("bucketacls")] 15 | public class BucketAcl 16 | { 17 | #region Public-Members 18 | 19 | /// 20 | /// Database identifier. 21 | /// 22 | [JsonIgnore] 23 | [Column("id", true, DataTypes.Int, false)] 24 | public int Id { get; set; } = 0; 25 | 26 | /// 27 | /// GUID. 28 | /// 29 | [Column("guid", false, DataTypes.Nvarchar, 64, false)] 30 | public string GUID { get; set; } = Guid.NewGuid().ToString(); 31 | 32 | /// 33 | /// User group. 34 | /// 35 | [Column("usergroup", false, DataTypes.Nvarchar, 256, true)] 36 | public string UserGroup { get; set; } = null; 37 | 38 | /// 39 | /// Bucket GUID. 40 | /// 41 | [Column("bucketguid", false, DataTypes.Nvarchar, 64, true)] 42 | public string BucketGUID { get; set; } = null; 43 | 44 | /// 45 | /// User GUID. 46 | /// 47 | [Column("userguid", false, DataTypes.Nvarchar, 64, true)] 48 | public string UserGUID { get; set; } = null; 49 | 50 | /// 51 | /// GUID of the issuing user. 52 | /// 53 | [Column("issuedbyuserguid", false, DataTypes.Nvarchar, 64, true)] 54 | public string IssuedByUserGUID { get; set; } = null; 55 | 56 | /// 57 | /// Permit read operations. 58 | /// 59 | [Column("permitread", false, DataTypes.Boolean, false)] 60 | public bool PermitRead { get; set; } = false; 61 | 62 | /// 63 | /// Permit write operations. 64 | /// 65 | [Column("permitwrite", false, DataTypes.Boolean, false)] 66 | public bool PermitWrite { get; set; } = false; 67 | 68 | /// 69 | /// Permit access control read operations. 70 | /// 71 | [Column("permitreadacp", false, DataTypes.Boolean, false)] 72 | public bool PermitReadAcp { get; set; } = false; 73 | 74 | /// 75 | /// Permit access control write operations. 76 | /// 77 | [Column("permitwriteacp", false, DataTypes.Boolean, false)] 78 | public bool PermitWriteAcp { get; set; } = false; 79 | 80 | /// 81 | /// Permit full control. 82 | /// 83 | [Column("permitfullcontrol", false, DataTypes.Boolean, false)] 84 | public bool FullControl { get; set; } = false; 85 | 86 | /// 87 | /// Timestamp from record creation, in UTC time. 88 | /// 89 | [Column("createdutc", false, DataTypes.DateTime, 6, 6, false)] 90 | public DateTime CreatedUtc { get; set; } = DateTime.Now.ToUniversalTime(); 91 | 92 | #endregion 93 | 94 | #region Private-Members 95 | 96 | #endregion 97 | 98 | #region Constructors-and-Factories 99 | 100 | /// 101 | /// Instantiate. 102 | /// 103 | public BucketAcl() 104 | { 105 | 106 | } 107 | 108 | /// 109 | /// Create a group ACL. 110 | /// 111 | /// Group name. 112 | /// Issued by user GUID. 113 | /// Bucket GUID. 114 | /// Permit read. 115 | /// Permit write. 116 | /// Permit access control read. 117 | /// Permit access control write. 118 | /// Full control. 119 | /// Instance. 120 | public static BucketAcl GroupAcl( 121 | string groupName, 122 | string issuedByUserGuid, 123 | string bucketGuid, 124 | bool permitRead, 125 | bool permitWrite, 126 | bool permitReadAcp, 127 | bool permitWriteAcp, 128 | bool fullControl) 129 | { 130 | if (String.IsNullOrEmpty(groupName)) throw new ArgumentNullException(nameof(groupName)); 131 | if (String.IsNullOrEmpty(issuedByUserGuid)) throw new ArgumentNullException(nameof(issuedByUserGuid)); 132 | if (String.IsNullOrEmpty(bucketGuid)) throw new ArgumentNullException(nameof(bucketGuid)); 133 | 134 | BucketAcl ret = new BucketAcl(); 135 | 136 | ret.UserGroup = groupName; 137 | ret.UserGUID = null; 138 | ret.IssuedByUserGUID = issuedByUserGuid; 139 | ret.BucketGUID = bucketGuid; 140 | 141 | ret.PermitRead = permitRead; 142 | ret.PermitWrite = permitWrite; 143 | ret.PermitReadAcp = permitReadAcp; 144 | ret.PermitWriteAcp = permitWriteAcp; 145 | ret.FullControl = fullControl; 146 | 147 | return ret; 148 | } 149 | 150 | /// 151 | /// Create a user ACL. 152 | /// 153 | /// User GUID. 154 | /// Issued by user GUID. 155 | /// Bucket GUID. 156 | /// Permit read. 157 | /// Permit write. 158 | /// Permit access control read. 159 | /// Permit access control write. 160 | /// Full control. 161 | /// Instance. 162 | public static BucketAcl UserAcl( 163 | string userGuid, 164 | string issuedByUserGuid, 165 | string bucketGuid, 166 | bool permitRead, 167 | bool permitWrite, 168 | bool permitReadAcp, 169 | bool permitWriteAcp, 170 | bool fullControl) 171 | { 172 | if (String.IsNullOrEmpty(userGuid)) throw new ArgumentNullException(nameof(userGuid)); 173 | if (String.IsNullOrEmpty(issuedByUserGuid)) throw new ArgumentNullException(nameof(issuedByUserGuid)); 174 | if (String.IsNullOrEmpty(bucketGuid)) throw new ArgumentNullException(nameof(bucketGuid)); 175 | 176 | BucketAcl ret = new BucketAcl(); 177 | 178 | ret.UserGroup = null; 179 | ret.UserGUID = userGuid; 180 | ret.IssuedByUserGUID = issuedByUserGuid; 181 | ret.BucketGUID = bucketGuid; 182 | 183 | ret.PermitRead = permitRead; 184 | ret.PermitWrite = permitWrite; 185 | ret.PermitReadAcp = permitReadAcp; 186 | ret.PermitWriteAcp = permitWriteAcp; 187 | ret.FullControl = fullControl; 188 | 189 | return ret; 190 | } 191 | 192 | #endregion 193 | 194 | #region Public-Methods 195 | 196 | /// 197 | /// Human-readable string of the object. 198 | /// 199 | /// 200 | public override string ToString() 201 | { 202 | string 203 | ret = "--- Bucket ACL " + Id + " ---" + Environment.NewLine + 204 | " User group : " + UserGroup + Environment.NewLine + 205 | " User GUID : " + UserGUID + Environment.NewLine + 206 | " Issued by : " + IssuedByUserGUID + Environment.NewLine + 207 | " Permissions : " + Environment.NewLine + 208 | " READ : " + PermitRead.ToString() + Environment.NewLine + 209 | " WRITE : " + PermitWrite.ToString() + Environment.NewLine + 210 | " READ_ACP : " + PermitReadAcp.ToString() + Environment.NewLine + 211 | " WRITE_ACP : " + PermitWriteAcp.ToString() + Environment.NewLine + 212 | " FULL_CONTROL : " + FullControl.ToString() + Environment.NewLine; 213 | 214 | return ret; 215 | } 216 | 217 | #endregion 218 | 219 | #region Private-Methods 220 | 221 | #endregion 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/Less3/Classes/BucketManager.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Classes 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | using SyslogLogging; 12 | using Watson.ORM; 13 | 14 | /// 15 | /// Bucket manager. 16 | /// 17 | public class BucketManager 18 | { 19 | #region Public-Members 20 | 21 | #endregion 22 | 23 | #region Private-Members 24 | 25 | private Settings _Settings; 26 | private LoggingModule _Logging; 27 | private ConfigManager _Config; 28 | private WatsonORM _ORM; 29 | 30 | private readonly object _BucketsLock = new object(); 31 | private List _Buckets = new List(); 32 | 33 | #endregion 34 | 35 | #region Constructors-and-Factories 36 | 37 | internal BucketManager(Settings settings, LoggingModule logging, ConfigManager config, WatsonORM orm) 38 | { 39 | _Settings = settings ?? throw new ArgumentNullException(nameof(settings)); 40 | _Logging = logging ?? throw new ArgumentNullException(nameof(logging)); 41 | _Config = config ?? throw new ArgumentNullException(nameof(config)); 42 | _ORM = orm ?? throw new ArgumentNullException(nameof(orm)); 43 | 44 | InitializeBuckets(); 45 | } 46 | 47 | #endregion 48 | 49 | #region Internal-Methods 50 | 51 | internal bool Add(Bucket bucket) 52 | { 53 | if (bucket == null) throw new ArgumentNullException(nameof(bucket)); 54 | 55 | bool success = _Config.AddBucket(bucket); 56 | if (success) 57 | { 58 | BucketClient client = new BucketClient(_Settings, _Logging, bucket, _ORM); 59 | 60 | lock (_BucketsLock) 61 | { 62 | _Buckets.Add(client); 63 | } 64 | 65 | InitializeBucket(bucket); 66 | } 67 | 68 | return success; 69 | } 70 | 71 | internal bool Remove(Bucket bucket, bool destroy) 72 | { 73 | if (bucket == null) throw new ArgumentNullException(nameof(bucket)); 74 | 75 | bool removed = false; 76 | 77 | if (_Config.BucketExists(bucket.Name)) 78 | { 79 | BucketClient client = GetClient(bucket.Name); 80 | if (client != null) 81 | { 82 | lock (_BucketsLock) 83 | { 84 | List clients = _Buckets.Where(b => !b.Name.Equals(bucket.Name)).ToList(); 85 | _Buckets = new List(clients); 86 | client.Dispose(); 87 | client = null; 88 | } 89 | } 90 | 91 | removed = true; 92 | 93 | _Config.DeleteBucket(bucket.GUID); 94 | } 95 | 96 | if (removed) 97 | { 98 | if (destroy) 99 | { 100 | if (!Destroy(bucket)) 101 | _Logging.Warn("BucketManager Remove issues encountered removing data for bucket " + bucket.Name + ", cleanup required"); 102 | else 103 | _Logging.Info("BucketManager Remove removed bucket with name " + bucket.Name + " with owner " + bucket.OwnerGUID); 104 | } 105 | else 106 | { 107 | _Logging.Info("BucketManager Remove removed bucket with name " + bucket.Name + " with owner " + bucket.OwnerGUID); 108 | } 109 | 110 | return true; 111 | } 112 | else 113 | { 114 | _Logging.Warn("BucketManager Remove bucket with name " + bucket.Name + " not found"); 115 | return false; 116 | } 117 | } 118 | 119 | internal Bucket GetByName(string bucketName) 120 | { 121 | if (String.IsNullOrEmpty(bucketName)) throw new ArgumentNullException(nameof(bucketName)); 122 | return _Config.GetBucketByName(bucketName); 123 | } 124 | 125 | internal Bucket GetByGuid(string guid) 126 | { 127 | if (String.IsNullOrEmpty(guid)) throw new ArgumentNullException(nameof(guid)); 128 | return _Config.GetBucketByGuid(guid); 129 | } 130 | 131 | internal BucketClient GetClient(string bucketName) 132 | { 133 | if (String.IsNullOrEmpty(bucketName)) throw new ArgumentNullException(nameof(bucketName)); 134 | 135 | lock (_BucketsLock) 136 | { 137 | bool exists = _Buckets.Exists(b => b.Name.Equals(bucketName)); 138 | if (!exists) return null; 139 | return _Buckets.First(b => b.Name.Equals(bucketName)); 140 | } 141 | } 142 | 143 | internal List GetUserBuckets(string userGuid) 144 | { 145 | if (String.IsNullOrEmpty(userGuid)) throw new ArgumentNullException(nameof(userGuid)); 146 | return _Config.GetBucketsByUser(userGuid); 147 | } 148 | 149 | #endregion 150 | 151 | #region Private-Methods 152 | 153 | private void InitializeBuckets() 154 | { 155 | List buckets = _Config.GetBuckets(); 156 | 157 | if (buckets != null && buckets.Count > 0) 158 | { 159 | foreach (Bucket curr in buckets) 160 | { 161 | InitializeBucket(curr); 162 | } 163 | } 164 | } 165 | 166 | private void InitializeBucket(Bucket bucket) 167 | { 168 | lock (_BucketsLock) 169 | { 170 | BucketClient client = new BucketClient(_Settings, _Logging, bucket, _ORM); 171 | _Buckets.Add(client); 172 | } 173 | } 174 | 175 | private bool Destroy(Bucket bucket) 176 | { 177 | #region Delete-Object-Files 178 | 179 | bool objectFilesDelete = false; 180 | try 181 | { 182 | if (Directory.Exists(bucket.DiskDirectory)) 183 | ClearDirectory(bucket.DiskDirectory); 184 | objectFilesDelete = true; 185 | } 186 | catch (Exception) 187 | { 188 | 189 | } 190 | 191 | #endregion 192 | 193 | #region Remove-Objects-Directory 194 | 195 | bool objectsDirectoryDelete = false; 196 | try 197 | { 198 | if (Directory.Exists(bucket.DiskDirectory)) 199 | Directory.Delete(bucket.DiskDirectory); 200 | objectsDirectoryDelete = true; 201 | } 202 | catch (Exception) 203 | { 204 | 205 | } 206 | 207 | #endregion 208 | 209 | #region Delete-Root-Files 210 | 211 | bool rootFilesDelete = false; 212 | 213 | try 214 | { 215 | if (Directory.Exists(_Settings.Storage.DiskDirectory + bucket.Name)) 216 | { 217 | ClearDirectory(_Settings.Storage.DiskDirectory + bucket.Name); 218 | rootFilesDelete = true; 219 | } 220 | } 221 | catch (Exception) 222 | { 223 | 224 | } 225 | 226 | #endregion 227 | 228 | #region Remove-Root-Directory 229 | 230 | bool rootDirectoryDelete = false; 231 | 232 | try 233 | { 234 | if (Directory.Exists(_Settings.Storage.DiskDirectory + bucket.Name)) 235 | { 236 | Directory.Delete(_Settings.Storage.DiskDirectory + bucket.Name); 237 | rootDirectoryDelete = true; 238 | } 239 | } 240 | catch (Exception) 241 | { 242 | 243 | } 244 | 245 | #endregion 246 | 247 | _Logging.Info("Destroy bucket " + bucket.Name + ": " + 248 | "obj files [" + objectFilesDelete + "] " + 249 | "obj dir [" + objectsDirectoryDelete + "] " + 250 | "root files [" + rootFilesDelete + "] " + 251 | "root dir [" + rootDirectoryDelete + "]"); 252 | 253 | return objectFilesDelete && objectsDirectoryDelete && rootFilesDelete && rootDirectoryDelete; 254 | } 255 | 256 | private void ClearDirectory(string path) 257 | { 258 | DirectoryInfo dir = new DirectoryInfo(path); 259 | foreach (FileInfo fi in dir.EnumerateFiles()) 260 | { 261 | fi.Delete(); 262 | } 263 | } 264 | 265 | #endregion 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/Less3/Classes/BucketStatistics.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Classes 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | /// 8 | /// Bucket statistics. 9 | /// 10 | public class BucketStatistics 11 | { 12 | #region Public-Members 13 | 14 | /// 15 | /// The name of the bucket. 16 | /// 17 | public string Name { get; set; } = null; 18 | 19 | /// 20 | /// GUID of the bucket. 21 | /// 22 | public string GUID { get; set; } = Guid.NewGuid().ToString(); 23 | 24 | /// 25 | /// The number of objects in the bucket including all versions. 26 | /// 27 | public long Objects = 0; 28 | 29 | /// 30 | /// The number of bytes for all objects in the bucket. 31 | /// 32 | public long Bytes = 0; 33 | 34 | #endregion 35 | 36 | #region Private-Members 37 | 38 | #endregion 39 | 40 | #region Constructors-and-Factories 41 | 42 | /// 43 | /// Instantiate. 44 | /// 45 | public BucketStatistics() 46 | { 47 | 48 | } 49 | 50 | /// 51 | /// Instantiate. 52 | /// 53 | /// Name. 54 | /// GUID. 55 | /// Number of objects. 56 | /// Number of bytes. 57 | public BucketStatistics(string name, string guid, long objects, long bytes) 58 | { 59 | 60 | } 61 | 62 | #endregion 63 | 64 | #region Public-Methods 65 | 66 | #endregion 67 | 68 | #region Private-Methods 69 | 70 | #endregion 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Less3/Classes/BucketTag.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Classes 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Data; 6 | using System.Text; 7 | using System.Text.Json; 8 | using System.Text.Json.Serialization; 9 | using Watson.ORM.Core; 10 | 11 | /// 12 | /// Tag entry for a bucket. 13 | /// 14 | [Table("buckettags")] 15 | public class BucketTag 16 | { 17 | #region Public-Members 18 | 19 | /// 20 | /// Database identifier. 21 | /// 22 | [JsonIgnore] 23 | [Column("id", true, DataTypes.Int, false)] 24 | public int Id { get; set; } = 0; 25 | 26 | /// 27 | /// GUID. 28 | /// 29 | [Column("guid", false, DataTypes.Nvarchar, 64, false)] 30 | public string GUID { get; set; } = Guid.NewGuid().ToString(); 31 | 32 | /// 33 | /// GUID of the bucket. 34 | /// 35 | [Column("bucketguid", false, DataTypes.Nvarchar, 64, false)] 36 | public string BucketGUID { get; set; } = null; 37 | 38 | /// 39 | /// Key. 40 | /// 41 | [Column("tagkey", false, DataTypes.Nvarchar, 256, false)] 42 | public string Key { get; set; } = null; 43 | 44 | /// 45 | /// Value. 46 | /// 47 | [Column("tagvalue", false, DataTypes.Nvarchar, 1024, true)] 48 | public string Value { get; set; } = null; 49 | 50 | /// 51 | /// Timestamp from record creation, in UTC time. 52 | /// 53 | [Column("createdutc", false, DataTypes.DateTime, 6, 6, false)] 54 | public DateTime CreatedUtc { get; set; } = DateTime.Now.ToUniversalTime(); 55 | 56 | #endregion 57 | 58 | #region Private-Members 59 | 60 | #endregion 61 | 62 | #region Constructors-and-Factories 63 | 64 | /// 65 | /// Instantiate. 66 | /// 67 | public BucketTag() 68 | { 69 | 70 | } 71 | 72 | /// 73 | /// Instantiate. 74 | /// 75 | /// Bucket GUID. 76 | /// Key. 77 | /// Value. 78 | public BucketTag(string bucketGuid, string key, string val) 79 | { 80 | if (String.IsNullOrEmpty(bucketGuid)) throw new ArgumentNullException(nameof(bucketGuid)); 81 | if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); 82 | 83 | BucketGUID = bucketGuid; 84 | Key = key; 85 | Value = val; 86 | } 87 | 88 | /// 89 | /// Instantiate. 90 | /// 91 | /// GUID. 92 | /// Bucket GUID. 93 | /// Key. 94 | /// Value. 95 | public BucketTag(string guid, string bucketGuid, string key, string val) 96 | { 97 | if (String.IsNullOrEmpty(guid)) throw new ArgumentNullException(nameof(guid)); 98 | if (String.IsNullOrEmpty(bucketGuid)) throw new ArgumentNullException(nameof(bucketGuid)); 99 | if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); 100 | 101 | GUID = guid; 102 | BucketGUID = bucketGuid; 103 | Key = key; 104 | Value = val; 105 | } 106 | 107 | #endregion 108 | 109 | #region Public-Methods 110 | 111 | #endregion 112 | 113 | #region Private-Methods 114 | 115 | #endregion 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Less3/Classes/ConfigManager.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Classes 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Data; 6 | using System.Text; 7 | 8 | using DatabaseWrapper.Core; 9 | using ExpressionTree; 10 | using SyslogLogging; 11 | using Watson.ORM; 12 | using Watson.ORM.Core; 13 | 14 | /// 15 | /// Configuration manager. 16 | /// 17 | internal class ConfigManager 18 | { 19 | #region Public-Members 20 | 21 | #endregion 22 | 23 | #region Private-Members 24 | 25 | private Settings _Settings = null; 26 | private LoggingModule _Logging = null; 27 | private WatsonORM _ORM = null; 28 | 29 | #endregion 30 | 31 | #region Constructors-and-Factories 32 | 33 | internal ConfigManager(Settings settings, LoggingModule logging, WatsonORM orm) 34 | { 35 | _Settings = settings ?? throw new ArgumentNullException(nameof(settings)); 36 | _Logging = logging ?? throw new ArgumentNullException(nameof(logging)); 37 | _ORM = orm ?? throw new ArgumentNullException(nameof(orm)); 38 | } 39 | 40 | #endregion 41 | 42 | #region Public-Methods 43 | 44 | #endregion 45 | 46 | #region Internal-User-Methods 47 | 48 | internal List GetUsers() 49 | { 50 | Expr e = new Expr( 51 | _ORM.GetColumnName(nameof(User.Id)), 52 | OperatorEnum.GreaterThan, 53 | 0); 54 | return _ORM.SelectMany(e); 55 | } 56 | 57 | internal bool UserGuidExists(string guid) 58 | { 59 | if (String.IsNullOrEmpty(guid)) throw new ArgumentNullException(nameof(guid)); 60 | 61 | Expr e = new Expr( 62 | _ORM.GetColumnName(nameof(User.GUID)), 63 | OperatorEnum.Equals, 64 | guid); 65 | 66 | User user = _ORM.SelectFirst(e); 67 | if (user != null) return true; 68 | return false; 69 | } 70 | 71 | internal bool UserEmailExists(string email) 72 | { 73 | if (String.IsNullOrEmpty(email)) throw new ArgumentNullException(nameof(email)); 74 | 75 | Expr e = new Expr( 76 | _ORM.GetColumnName(nameof(User.Email)), 77 | OperatorEnum.Equals, 78 | email); 79 | 80 | User user = _ORM.SelectFirst(e); 81 | if (user != null) return true; 82 | return false; 83 | } 84 | 85 | internal User GetUserByGuid(string guid) 86 | { 87 | if (String.IsNullOrEmpty(guid)) throw new ArgumentNullException(nameof(guid)); 88 | 89 | Expr e = new Expr( 90 | _ORM.GetColumnName(nameof(User.GUID)), 91 | OperatorEnum.Equals, 92 | guid); 93 | 94 | return _ORM.SelectFirst(e); 95 | } 96 | 97 | internal User GetUserByName(string name) 98 | { 99 | if (String.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); 100 | 101 | Expr e = new Expr( 102 | _ORM.GetColumnName(nameof(User.Name)), 103 | OperatorEnum.Equals, 104 | name); 105 | 106 | return _ORM.SelectFirst(e); 107 | } 108 | 109 | internal User GetUserByEmail(string email) 110 | { 111 | if (String.IsNullOrEmpty(email)) throw new ArgumentNullException(nameof(email)); 112 | 113 | Expr e = new Expr( 114 | _ORM.GetColumnName(nameof(User.Email)), 115 | OperatorEnum.Equals, 116 | email); 117 | 118 | return _ORM.SelectFirst(e); 119 | } 120 | 121 | internal User GetUserByAccessKey(string accessKey) 122 | { 123 | if (String.IsNullOrEmpty(accessKey)) throw new ArgumentNullException(nameof(accessKey)); 124 | 125 | Credential cred = GetCredentialByAccessKey(accessKey); 126 | if (cred == null) 127 | { 128 | _Logging.Warn("ConfigManager GetUserByAccessKey access key " + accessKey + " not found"); 129 | return null; 130 | } 131 | 132 | User user = GetUserByGuid(cred.UserGUID); 133 | if (user == null) 134 | { 135 | _Logging.Warn("ConfigManager GetUserByAccessKey user GUID " + cred.UserGUID + " not found, referenced by credential GUID " + cred.GUID); 136 | return null; 137 | } 138 | 139 | return user; 140 | } 141 | 142 | internal bool AddUser(string guid, string name, string email) 143 | { 144 | if (String.IsNullOrEmpty(guid)) throw new ArgumentNullException(nameof(guid)); 145 | if (String.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); 146 | if (String.IsNullOrEmpty(email)) throw new ArgumentNullException(nameof(email)); 147 | 148 | User user = new User(guid, name, email); 149 | return AddUser(user); 150 | } 151 | 152 | internal bool AddUser(User user) 153 | { 154 | if (user == null) throw new ArgumentNullException(nameof(user)); 155 | 156 | User userByGuid = GetUserByGuid(user.GUID); 157 | if (userByGuid != null) 158 | { 159 | _Logging.Warn("ConfigManager AddUser user GUID " + user.GUID + " already exists"); 160 | return false; 161 | } 162 | 163 | User userByEmail = GetUserByEmail(user.Email); 164 | if (userByEmail != null) 165 | { 166 | _Logging.Warn("ConfigManager AddUser user email " + user.Email + " already exists"); 167 | return false; 168 | } 169 | 170 | _ORM.Insert(user); 171 | return true; 172 | } 173 | 174 | internal void DeleteUser(string guid) 175 | { 176 | if (String.IsNullOrEmpty(guid)) throw new ArgumentNullException(nameof(guid)); 177 | User tempUser = GetUserByGuid(guid); 178 | if (tempUser != null) 179 | { 180 | _ORM.Delete(tempUser); 181 | } 182 | } 183 | 184 | #endregion 185 | 186 | #region Internal-Credential-Methods 187 | 188 | internal List GetCredentials() 189 | { 190 | Expr e = new Expr( 191 | _ORM.GetColumnName(nameof(Credential.Id)), 192 | OperatorEnum.GreaterThan, 193 | 0); 194 | return _ORM.SelectMany(e); 195 | } 196 | 197 | internal bool CredentialGuidExists(string guid) 198 | { 199 | if (String.IsNullOrEmpty(guid)) throw new ArgumentNullException(nameof(guid)); 200 | 201 | Expr e = new Expr( 202 | _ORM.GetColumnName(nameof(Credential.GUID)), 203 | OperatorEnum.Equals, 204 | guid); 205 | 206 | Credential cred = _ORM.SelectFirst(e); 207 | if (cred != null) return true; 208 | return false; 209 | } 210 | 211 | internal Credential GetCredentialByGuid(string guid) 212 | { 213 | if (String.IsNullOrEmpty(guid)) throw new ArgumentNullException(nameof(guid)); 214 | 215 | Expr e = new Expr( 216 | _ORM.GetColumnName(nameof(Credential.GUID)), 217 | OperatorEnum.Equals, 218 | guid); 219 | 220 | return _ORM.SelectFirst(e); 221 | } 222 | 223 | internal List GetCredentialsByUser(string userGuid) 224 | { 225 | if (String.IsNullOrEmpty(userGuid)) throw new ArgumentNullException(nameof(userGuid)); 226 | 227 | Expr e = new Expr( 228 | _ORM.GetColumnName(nameof(Credential.UserGUID)), 229 | OperatorEnum.Equals, 230 | userGuid); 231 | 232 | return _ORM.SelectMany(e); 233 | } 234 | 235 | internal Credential GetCredentialByAccessKey(string accessKey) 236 | { 237 | if (String.IsNullOrEmpty(accessKey)) throw new ArgumentNullException(nameof(accessKey)); 238 | 239 | Expr e = new Expr( 240 | _ORM.GetColumnName(nameof(Credential.AccessKey)), 241 | OperatorEnum.Equals, 242 | accessKey); 243 | 244 | return _ORM.SelectFirst(e); 245 | } 246 | 247 | internal bool AddCredential(string userGuid, string description, string accessKey, string secretKey, bool isBase64) 248 | { 249 | if (String.IsNullOrEmpty(userGuid)) throw new ArgumentNullException(nameof(userGuid)); 250 | if (String.IsNullOrEmpty(accessKey)) throw new ArgumentNullException(nameof(accessKey)); 251 | if (String.IsNullOrEmpty(secretKey)) throw new ArgumentNullException(nameof(secretKey)); 252 | 253 | Credential cred = new Credential(userGuid, description, accessKey, secretKey, isBase64); 254 | return AddCredential(cred); 255 | } 256 | 257 | internal bool AddCredential(Credential cred) 258 | { 259 | if (cred == null) throw new ArgumentNullException(nameof(cred)); 260 | 261 | Credential credByGuid = GetCredentialByGuid(cred.GUID); 262 | if (credByGuid != null) 263 | { 264 | _Logging.Warn("ConfigManager AddCredential credential GUID " + cred.GUID + " already exists"); 265 | return false; 266 | } 267 | 268 | Credential credByKey = GetCredentialByAccessKey(cred.AccessKey); 269 | if (credByKey != null) 270 | { 271 | _Logging.Warn("ConfigManager AddCredential access key " + cred.AccessKey + " already exists"); 272 | return false; 273 | } 274 | 275 | _ORM.Insert(cred); 276 | return true; 277 | } 278 | 279 | internal void DeleteCredential(string guid) 280 | { 281 | if (String.IsNullOrEmpty(guid)) throw new ArgumentNullException(nameof(guid)); 282 | Credential tempCred = GetCredentialByGuid(guid); 283 | if (tempCred != null) 284 | { 285 | _ORM.Delete(tempCred); 286 | } 287 | } 288 | 289 | #endregion 290 | 291 | #region Internal-Bucket-Methods 292 | 293 | internal List GetBuckets() 294 | { 295 | Expr e = new Expr( 296 | _ORM.GetColumnName(nameof(Bucket.Id)), 297 | OperatorEnum.GreaterThan, 298 | 0); 299 | 300 | return _ORM.SelectMany(e); 301 | } 302 | 303 | internal bool BucketExists(string name) 304 | { 305 | if (String.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); 306 | 307 | Expr e = new Expr( 308 | _ORM.GetColumnName(nameof(Bucket.Name)), 309 | OperatorEnum.Equals, 310 | name); 311 | 312 | Bucket bucket = _ORM.SelectFirst(e); 313 | if (bucket != null) return true; 314 | return false; 315 | } 316 | 317 | internal List GetBucketsByUser(string userGuid) 318 | { 319 | if (String.IsNullOrEmpty(userGuid)) throw new ArgumentNullException(nameof(userGuid)); 320 | 321 | Expr e = new Expr( 322 | _ORM.GetColumnName(nameof(Bucket.OwnerGUID)), 323 | OperatorEnum.Equals, 324 | userGuid); 325 | 326 | return _ORM.SelectMany(e); 327 | } 328 | 329 | internal Bucket GetBucketByGuid(string guid) 330 | { 331 | if (String.IsNullOrEmpty(guid)) throw new ArgumentNullException(nameof(guid)); 332 | 333 | Expr e = new Expr( 334 | _ORM.GetColumnName(nameof(Bucket.GUID)), 335 | OperatorEnum.Equals, 336 | guid); 337 | 338 | return _ORM.SelectFirst(e); 339 | } 340 | 341 | internal Bucket GetBucketByName(string name) 342 | { 343 | if (String.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); 344 | 345 | Expr e = new Expr( 346 | _ORM.GetColumnName(nameof(Bucket.Name)), 347 | OperatorEnum.Equals, 348 | name); 349 | 350 | return _ORM.SelectFirst(e); 351 | } 352 | 353 | internal bool AddBucket(string userGuid, string name) 354 | { 355 | if (String.IsNullOrEmpty(userGuid)) throw new ArgumentNullException(nameof(userGuid)); 356 | if (String.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); 357 | 358 | Bucket bucket = new Bucket( 359 | Guid.NewGuid().ToString(), 360 | name, 361 | userGuid, 362 | _Settings.Storage.StorageType, 363 | _Settings.Storage.DiskDirectory + name + "/Objects"); 364 | 365 | return AddBucket(bucket); 366 | } 367 | 368 | internal bool AddBucket(Bucket bucket) 369 | { 370 | if (bucket == null) throw new ArgumentNullException(nameof(bucket)); 371 | 372 | if (BucketExists(bucket.Name)) 373 | { 374 | _Logging.Warn("ConfigManager AddBucket bucket " + bucket.Name + " already exists"); 375 | return false; 376 | } 377 | 378 | _ORM.Insert(bucket); 379 | return true; 380 | } 381 | 382 | internal void DeleteBucket(string guid) 383 | { 384 | if (String.IsNullOrEmpty(guid)) throw new ArgumentNullException(nameof(guid)); 385 | Bucket bucket = GetBucketByGuid(guid); 386 | if (bucket != null) 387 | { 388 | _ORM.Delete(bucket); 389 | } 390 | } 391 | 392 | #endregion 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /src/Less3/Classes/ConsoleManager.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Classes 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using SyslogLogging; 11 | 12 | /// 13 | /// Console for less3. 14 | /// 15 | internal class ConsoleManager 16 | { 17 | #region Public-Members 18 | 19 | #endregion 20 | 21 | #region Private-Members 22 | 23 | private bool _Enabled { get; set; } 24 | private Settings _Settings { get; set; } 25 | private LoggingModule _Logging { get; set; } 26 | 27 | #endregion 28 | 29 | #region Constructors-and-Factories 30 | 31 | internal ConsoleManager( 32 | Settings settings, 33 | LoggingModule logging) 34 | { 35 | if (settings == null) throw new ArgumentNullException(nameof(settings)); 36 | if (logging == null) throw new ArgumentNullException(nameof(logging)); 37 | 38 | _Enabled = true; 39 | 40 | _Settings = settings; 41 | _Logging = logging; 42 | } 43 | 44 | #endregion 45 | 46 | #region Public-Methods 47 | 48 | internal void Worker() 49 | { 50 | string userInput = ""; 51 | 52 | while (_Enabled) 53 | { 54 | Console.Write("Command (? for help) > "); 55 | userInput = Console.ReadLine(); 56 | 57 | if (userInput == null) continue; 58 | switch (userInput.ToLower().Trim()) 59 | { 60 | case "?": 61 | Menu(); 62 | break; 63 | 64 | case "c": 65 | case "cls": 66 | case "clear": 67 | Console.Clear(); 68 | break; 69 | 70 | case "q": 71 | case "quit": 72 | _Enabled = false; 73 | break; 74 | } 75 | } 76 | } 77 | 78 | #endregion 79 | 80 | #region Private-Methods 81 | 82 | private void Menu() 83 | { 84 | Console.WriteLine(Common.Line(79, "-")); 85 | Console.WriteLine(" ? help / this menu"); 86 | Console.WriteLine(" cls / c clear the console"); 87 | Console.WriteLine(" quit / q exit the application"); 88 | Console.WriteLine(""); 89 | return; 90 | } 91 | 92 | #endregion 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Less3/Classes/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Classes 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | internal static class Constants 8 | { 9 | // http://loveascii.com/hearts.html 10 | // http://patorjk.com/software/taag/#p=display&f=Small&t=less3 11 | internal static string Logo = 12 | @" ,d88b.d88b, _ ____ " + Environment.NewLine + 13 | @" 88888888888 | |___ _____|__ / " + Environment.NewLine + 14 | @" `Y8888888Y' | / -_|_-<_-<|_ \ " + Environment.NewLine + 15 | @" `Y888Y' |_\___/__/__/___/ " + Environment.NewLine + 16 | @" `Y' " + Environment.NewLine; 17 | 18 | internal static class Headers 19 | { 20 | internal static string RequestType = "X-Request-Type"; 21 | internal static string AuthenticationResult = "X-Authentication-Result"; 22 | internal static string AuthorizedBy = "X-Authorized-By"; 23 | 24 | internal static string DeleteMarker = "X-Amz-Delete-Marker"; 25 | internal static string AccessControlList = "X-Amz-Acl"; 26 | internal static string AclGrantRead = "X-Amz-Grant-Read"; 27 | internal static string AclGrantWrite = "X-Amz-Grant-Write"; 28 | internal static string AclGrantReadAcp = "X-Amz-Grant-Read-Acp"; 29 | internal static string AclGrantWriteAcp = "X-Amz-Grant-Write-Acp"; 30 | internal static string AclGrantFullControl = "X-Amz-Grant-Full-Control"; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Less3/Classes/Credential.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Classes 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Data; 6 | using System.Text; 7 | using System.Text.Json; 8 | using System.Text.Json.Serialization; 9 | using Watson.ORM.Core; 10 | 11 | /// 12 | /// Credential. 13 | /// 14 | [Table("credential")] 15 | public class Credential 16 | { 17 | #region Public-Members 18 | 19 | /// 20 | /// Database identifier. 21 | /// 22 | [JsonIgnore] 23 | [Column("id", true, DataTypes.Int, false)] 24 | public int Id { get; set; } = 0; 25 | 26 | /// 27 | /// GUID of the credential. 28 | /// 29 | [Column("guid", false, DataTypes.Nvarchar, 64, false)] 30 | public string GUID { get; set; } = Guid.NewGuid().ToString(); 31 | 32 | /// 33 | /// User GUID. 34 | /// 35 | [Column("userguid", false, DataTypes.Nvarchar, 64, false)] 36 | public string UserGUID { get; set; } = Guid.NewGuid().ToString(); 37 | 38 | /// 39 | /// Description. 40 | /// 41 | [Column("description", false, DataTypes.Nvarchar, 256, true)] 42 | public string Description { get; set; } = null; 43 | 44 | /// 45 | /// Access key. 46 | /// 47 | [Column("accesskey", false, DataTypes.Nvarchar, 256, false)] 48 | public string AccessKey { get; set; } = null; 49 | 50 | /// 51 | /// Secret key. 52 | /// 53 | [Column("secretkey", false, DataTypes.Nvarchar, 256, false)] 54 | public string SecretKey { get; set; } = null; 55 | 56 | /// 57 | /// Indicates if the secret key is base64 encoded. 58 | /// 59 | [Column("isbase64", false, DataTypes.Boolean, false)] 60 | public bool IsBase64 { get; set; } = false; 61 | 62 | /// 63 | /// Timestamp from record creation, in UTC time. 64 | /// 65 | [Column("createdutc", false, DataTypes.DateTime, 6, 6, false)] 66 | public DateTime CreatedUtc { get; set; } = DateTime.Now.ToUniversalTime(); 67 | 68 | #endregion 69 | 70 | #region Private-Members 71 | 72 | #endregion 73 | 74 | #region Constructors-and-Factories 75 | 76 | /// 77 | /// Instantiate. 78 | /// 79 | public Credential() 80 | { 81 | 82 | } 83 | 84 | /// 85 | /// Instantiate. 86 | /// 87 | /// User GUID. 88 | /// Description. 89 | /// Access key. 90 | /// Secret key. 91 | /// Is base64 encoded. 92 | public Credential(string userGuid, string description, string accessKey, string secretKey, bool isBase64) 93 | { 94 | if (String.IsNullOrEmpty(userGuid)) throw new ArgumentNullException(nameof(userGuid)); 95 | if (String.IsNullOrEmpty(accessKey)) throw new ArgumentNullException(nameof(accessKey)); 96 | if (String.IsNullOrEmpty(secretKey)) throw new ArgumentNullException(nameof(secretKey)); 97 | 98 | GUID = Guid.NewGuid().ToString(); 99 | UserGUID = userGuid; 100 | Description = description; 101 | AccessKey = accessKey; 102 | SecretKey = secretKey; 103 | IsBase64 = isBase64; 104 | } 105 | 106 | /// 107 | /// Instantiate. 108 | /// 109 | /// GUID. 110 | /// User GUID. 111 | /// Description. 112 | /// Access key. 113 | /// Secret key. 114 | /// Is base64 encoded. 115 | public Credential(string guid, string userGuid, string description, string accessKey, string secretKey, bool isBase64) 116 | { 117 | if (String.IsNullOrEmpty(guid)) throw new ArgumentNullException(nameof(guid)); 118 | if (String.IsNullOrEmpty(userGuid)) throw new ArgumentNullException(nameof(userGuid)); 119 | if (String.IsNullOrEmpty(accessKey)) throw new ArgumentNullException(nameof(accessKey)); 120 | if (String.IsNullOrEmpty(secretKey)) throw new ArgumentNullException(nameof(secretKey)); 121 | 122 | GUID = guid; 123 | UserGUID = userGuid; 124 | Description = description; 125 | AccessKey = accessKey; 126 | SecretKey = secretKey; 127 | IsBase64 = isBase64; 128 | } 129 | 130 | #endregion 131 | 132 | #region Public-Methods 133 | 134 | #endregion 135 | 136 | #region Private-Methods 137 | 138 | #endregion 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Less3/Classes/Obj.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Classes 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Data; 6 | using System.Text; 7 | using System.Text.Json; 8 | using System.Text.Json.Serialization; 9 | using Watson.ORM.Core; 10 | 11 | /// 12 | /// Object stored in Less3. 13 | /// 14 | [Table("objects")] 15 | public class Obj 16 | { 17 | #region Public-Members 18 | 19 | /// 20 | /// Database identifier. 21 | /// 22 | [JsonIgnore] 23 | [Column("id", true, DataTypes.Int, false)] 24 | public int Id { get; set; } = 0; 25 | 26 | /// 27 | /// GUID of the object. 28 | /// 29 | [Column("guid", false, DataTypes.Nvarchar, 64, false)] 30 | public string GUID { get; set; } = Guid.NewGuid().ToString(); 31 | 32 | /// 33 | /// GUID of the bucket. 34 | /// 35 | [Column("bucketguid", false, DataTypes.Nvarchar, 64, false)] 36 | public string BucketGUID { get; set; } = null; 37 | 38 | /// 39 | /// GUID of the owner. 40 | /// 41 | [Column("ownerguid", false, DataTypes.Nvarchar, 64, false)] 42 | public string OwnerGUID { get; set; } = null; 43 | 44 | /// 45 | /// GUID of the author. 46 | /// 47 | [Column("authorguid", false, DataTypes.Nvarchar, 64, false)] 48 | public string AuthorGUID { get; set; } = null; 49 | 50 | /// 51 | /// Object key. 52 | /// 53 | [Column("key", false, DataTypes.Nvarchar, 256, false)] 54 | public string Key { get; set; } = null; 55 | 56 | /// 57 | /// Content type. 58 | /// 59 | [Column("contenttype", false, DataTypes.Nvarchar, 128, true)] 60 | public string ContentType { get; set; } = "application/octet-stream"; 61 | 62 | /// 63 | /// Content length. 64 | /// 65 | [Column("contentlength", false, DataTypes.Long, false)] 66 | public long ContentLength { get; set; } = 0; 67 | 68 | /// 69 | /// Object version. 70 | /// 71 | [Column("version", false, DataTypes.Long, false)] 72 | public long Version { get; set; } = 1; 73 | 74 | /// 75 | /// ETag of the object. 76 | /// 77 | [Column("etag", false, DataTypes.Nvarchar, 64, true)] 78 | public string Etag { get; set; } = null; 79 | 80 | /// 81 | /// Retention type. 82 | /// 83 | [Column("retention", false, DataTypes.Enum, 32, true)] 84 | public RetentionType Retention { get; set; } = RetentionType.NONE; 85 | 86 | /// 87 | /// BLOB filename. 88 | /// 89 | [Column("blobfilename", false, DataTypes.Nvarchar, 256, false)] 90 | public string BlobFilename { get; set; } = null; 91 | 92 | /// 93 | /// Indicates if the object is a folder, i.e. ends with '/' and has a content length of 0. 94 | /// 95 | [Column("isfolder", false, DataTypes.Boolean, false)] 96 | public bool IsFolder { get; set; } = false; 97 | 98 | /// 99 | /// Delete marker. 100 | /// 101 | [Column("deletemarker", false, DataTypes.Boolean, false)] 102 | public bool DeleteMarker { get; set; } = false; 103 | 104 | /// 105 | /// MD5. 106 | /// 107 | [Column("md5", false, DataTypes.Nvarchar, 32, true)] 108 | public string Md5 { get; set; } = null; 109 | 110 | /// 111 | /// Creation timestamp. 112 | /// 113 | [Column("createdutc", false, DataTypes.DateTime, false)] 114 | public DateTime CreatedUtc { get; set; } = DateTime.Now.ToUniversalTime(); 115 | 116 | /// 117 | /// Last update timestamp. 118 | /// 119 | [Column("lastupdateutc", false, DataTypes.DateTime, false)] 120 | public DateTime LastUpdateUtc { get; set; } = DateTime.Now.ToUniversalTime(); 121 | 122 | /// 123 | /// Last access timestamp. 124 | /// 125 | [Column("lastaccessutc", false, DataTypes.DateTime, false)] 126 | public DateTime LastAccessUtc { get; set; } = DateTime.Now.ToUniversalTime(); 127 | 128 | /// 129 | /// Object expiration timestamp. 130 | /// 131 | [Column("expirationutc", false, DataTypes.DateTime, true)] 132 | public DateTime? ExpirationUtc = null; 133 | 134 | #endregion 135 | 136 | #region Private-Members 137 | 138 | #endregion 139 | 140 | #region Constructors-and-Factories 141 | 142 | /// 143 | /// Instantiate. 144 | /// 145 | public Obj() 146 | { 147 | 148 | } 149 | 150 | #endregion 151 | 152 | #region Public-Methods 153 | 154 | #endregion 155 | 156 | #region Private-Methods 157 | 158 | #endregion 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Less3/Classes/ObjectAcl.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Classes 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Data; 6 | using System.Text; 7 | using System.Text.Json; 8 | using System.Text.Json.Serialization; 9 | using Watson.ORM.Core; 10 | 11 | /// 12 | /// Access control list entry for an object. 13 | /// 14 | [Table("objectacls")] 15 | public class ObjectAcl 16 | { 17 | #region Public-Members 18 | 19 | /// 20 | /// Database identifier. 21 | /// 22 | [JsonIgnore] 23 | [Column("id", true, DataTypes.Int, false)] 24 | public int Id { get; set; } = 0; 25 | 26 | /// 27 | /// GUID. 28 | /// 29 | [Column("guid", false, DataTypes.Nvarchar, 64, false)] 30 | public string GUID { get; set; } = Guid.NewGuid().ToString(); 31 | 32 | /// 33 | /// User group. 34 | /// 35 | [Column("usergroup", false, DataTypes.Nvarchar, 256, true)] 36 | public string UserGroup { get; set; } = null; 37 | 38 | /// 39 | /// User GUID. 40 | /// 41 | [Column("userguid", false, DataTypes.Nvarchar, 64, true)] 42 | public string UserGUID { get; set; } = null; 43 | 44 | /// 45 | /// GUID of the issuing user. 46 | /// 47 | [Column("issuedbyuserguid", false, DataTypes.Nvarchar, 64, true)] 48 | public string IssuedByUserGUID { get; set; } = null; 49 | 50 | /// 51 | /// GUID of the bucket. 52 | /// 53 | [Column("bucketguid", false, DataTypes.Nvarchar, 64, false)] 54 | public string BucketGUID { get; set; } = null; 55 | 56 | /// 57 | /// GUID of the object. 58 | /// 59 | [Column("objectguid", false, DataTypes.Nvarchar, 64, false)] 60 | public string ObjectGUID { get; set; } = null; 61 | 62 | /// 63 | /// Permit read operations. 64 | /// 65 | [Column("permitread", false, DataTypes.Boolean, false)] 66 | public bool PermitRead { get; set; } = false; 67 | 68 | /// 69 | /// Permit write operations. 70 | /// 71 | [Column("permitwrite", false, DataTypes.Boolean, false)] 72 | public bool PermitWrite { get; set; } = false; 73 | 74 | /// 75 | /// Permit access control read operations. 76 | /// 77 | [Column("permitreadacp", false, DataTypes.Boolean, false)] 78 | public bool PermitReadAcp { get; set; } = false; 79 | 80 | /// 81 | /// Permit access control write operations. 82 | /// 83 | [Column("permitwriteacp", false, DataTypes.Boolean, false)] 84 | public bool PermitWriteAcp { get; set; } = false; 85 | 86 | /// 87 | /// Permit full control. 88 | /// 89 | [Column("permitfullcontrol", false, DataTypes.Boolean, false)] 90 | public bool FullControl { get; set; } = false; 91 | 92 | /// 93 | /// Timestamp from record creation, in UTC time. 94 | /// 95 | [Column("createdutc", false, DataTypes.DateTime, 6, 6, false)] 96 | public DateTime CreatedUtc { get; set; } = DateTime.Now.ToUniversalTime(); 97 | 98 | #endregion 99 | 100 | #region Private-Members 101 | 102 | #endregion 103 | 104 | #region Constructors-and-Factories 105 | 106 | /// 107 | /// Instantiate. 108 | /// 109 | public ObjectAcl() 110 | { 111 | 112 | } 113 | 114 | /// 115 | /// Create a group ACL. 116 | /// 117 | /// Group name. 118 | /// Issued by user GUID. 119 | /// Bucket GUID. 120 | /// Object GUID. 121 | /// Permit read. 122 | /// Permit write. 123 | /// Permit access control read. 124 | /// Permit access control write. 125 | /// Full control. 126 | /// Instance. 127 | public static ObjectAcl GroupAcl( 128 | string groupName, 129 | string issuedByUserGuid, 130 | string bucketGuid, 131 | string objectGuid, 132 | bool permitRead, 133 | bool permitWrite, 134 | bool permitReadAcp, 135 | bool permitWriteAcp, 136 | bool fullControl) 137 | { 138 | if (String.IsNullOrEmpty(groupName)) throw new ArgumentNullException(nameof(groupName)); 139 | if (String.IsNullOrEmpty(issuedByUserGuid)) throw new ArgumentNullException(nameof(issuedByUserGuid)); 140 | if (String.IsNullOrEmpty(bucketGuid)) throw new ArgumentNullException(nameof(bucketGuid)); 141 | if (String.IsNullOrEmpty(objectGuid)) throw new ArgumentNullException(nameof(objectGuid)); 142 | 143 | ObjectAcl ret = new ObjectAcl(); 144 | 145 | ret.UserGroup = groupName; 146 | ret.UserGUID = null; 147 | ret.IssuedByUserGUID = issuedByUserGuid; 148 | ret.BucketGUID = bucketGuid; 149 | ret.ObjectGUID = objectGuid; 150 | 151 | ret.PermitRead = permitRead; 152 | ret.PermitWrite = permitWrite; 153 | ret.PermitReadAcp = permitReadAcp; 154 | ret.PermitWriteAcp = permitWriteAcp; 155 | ret.FullControl = fullControl; 156 | 157 | return ret; 158 | } 159 | 160 | /// 161 | /// Create a user ACL. 162 | /// 163 | /// User GUID. 164 | /// Issued by user GUID. 165 | /// Bucket GUID. 166 | /// Object GUID. 167 | /// Permit read. 168 | /// Permit write. 169 | /// Permit access control read. 170 | /// Permit access control write. 171 | /// Full control. 172 | /// Instance. 173 | public static ObjectAcl UserAcl( 174 | string userGuid, 175 | string issuedByUserGuid, 176 | string bucketGuid, 177 | string objectGuid, 178 | bool permitRead, 179 | bool permitWrite, 180 | bool permitReadAcp, 181 | bool permitWriteAcp, 182 | bool fullControl) 183 | { 184 | if (String.IsNullOrEmpty(userGuid)) throw new ArgumentNullException(nameof(userGuid)); 185 | if (String.IsNullOrEmpty(issuedByUserGuid)) throw new ArgumentNullException(nameof(issuedByUserGuid)); 186 | if (String.IsNullOrEmpty(objectGuid)) throw new ArgumentNullException(nameof(objectGuid)); 187 | 188 | ObjectAcl ret = new ObjectAcl(); 189 | 190 | ret.UserGroup = null; 191 | ret.UserGUID = userGuid; 192 | ret.IssuedByUserGUID = issuedByUserGuid; 193 | ret.BucketGUID = bucketGuid; 194 | ret.ObjectGUID = objectGuid; 195 | 196 | ret.PermitRead = permitRead; 197 | ret.PermitWrite = permitWrite; 198 | ret.PermitReadAcp = permitReadAcp; 199 | ret.PermitWriteAcp = permitWriteAcp; 200 | ret.FullControl = fullControl; 201 | 202 | return ret; 203 | } 204 | 205 | #endregion 206 | 207 | #region Public-Methods 208 | 209 | /// 210 | /// Create a human-readable string of the object. 211 | /// 212 | /// String. 213 | public override string ToString() 214 | { 215 | string 216 | ret = "--- Object ACL " + Id + " ---" + Environment.NewLine + 217 | " User group : " + UserGroup + Environment.NewLine + 218 | " User GUID : " + UserGUID + Environment.NewLine + 219 | " Issued by : " + IssuedByUserGUID + Environment.NewLine + 220 | " Bucket GUID : " + BucketGUID + Environment.NewLine + 221 | " Object GUID : " + ObjectGUID + Environment.NewLine + 222 | " Permissions : " + Environment.NewLine + 223 | " READ : " + PermitRead.ToString() + Environment.NewLine + 224 | " WRITE : " + PermitWrite.ToString() + Environment.NewLine + 225 | " READ_ACP : " + PermitReadAcp.ToString() + Environment.NewLine + 226 | " WRITE_ACP : " + PermitWriteAcp.ToString() + Environment.NewLine + 227 | " FULL_CONTROL : " + FullControl.ToString() + Environment.NewLine; 228 | 229 | return ret; 230 | } 231 | 232 | #endregion 233 | 234 | #region Private-Methods 235 | 236 | #endregion 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/Less3/Classes/ObjectTag.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Classes 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Data; 6 | using System.Text; 7 | using System.Text.Json; 8 | using System.Text.Json.Serialization; 9 | using Watson.ORM.Core; 10 | 11 | /// 12 | /// Tag entry for an object. 13 | /// 14 | [Table("objecttags")] 15 | public class ObjectTag 16 | { 17 | #region Public-Members 18 | 19 | /// 20 | /// Database identifier. 21 | /// 22 | [JsonIgnore] 23 | [Column("id", true, DataTypes.Int, false)] 24 | public int Id { get; set; } = 0; 25 | 26 | /// 27 | /// GUID. 28 | /// 29 | [Column("guid", false, DataTypes.Nvarchar, 64, false)] 30 | public string GUID { get; set; } = Guid.NewGuid().ToString(); 31 | 32 | /// 33 | /// GUID of the bucket. 34 | /// 35 | [Column("bucketguid", false, DataTypes.Nvarchar, 64, false)] 36 | public string BucketGUID { get; set; } = Guid.NewGuid().ToString(); 37 | 38 | /// 39 | /// GUID of the object. 40 | /// 41 | [Column("objectguid", false, DataTypes.Nvarchar, 64, false)] 42 | public string ObjectGUID { get; set; } = Guid.NewGuid().ToString(); 43 | 44 | /// 45 | /// Key. 46 | /// 47 | [Column("tagkey", false, DataTypes.Nvarchar, 256, false)] 48 | public string Key { get; set; } = null; 49 | 50 | /// 51 | /// Value. 52 | /// 53 | [Column("tagvalue", false, DataTypes.Nvarchar, 1024, true)] 54 | public string Value { get; set; } = null; 55 | 56 | /// 57 | /// Timestamp from record creation, in UTC time. 58 | /// 59 | [Column("createdutc", false, DataTypes.DateTime, 6, 6, false)] 60 | public DateTime CreatedUtc { get; set; } = DateTime.Now.ToUniversalTime(); 61 | 62 | #endregion 63 | 64 | #region Private-Members 65 | 66 | #endregion 67 | 68 | #region Constructors-and-Factories 69 | 70 | /// 71 | /// Instantiate. 72 | /// 73 | public ObjectTag() 74 | { 75 | 76 | } 77 | 78 | /// 79 | /// Instantiate. 80 | /// 81 | /// Bucket GUID. 82 | /// Object GUID. 83 | /// Key. 84 | /// Value. 85 | public ObjectTag(string bucketGuid, string objectGuid, string key, string val) 86 | { 87 | if (String.IsNullOrEmpty(bucketGuid)) throw new ArgumentNullException(nameof(bucketGuid)); 88 | if (String.IsNullOrEmpty(objectGuid)) throw new ArgumentNullException(nameof(objectGuid)); 89 | if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); 90 | 91 | BucketGUID = bucketGuid; 92 | ObjectGUID = objectGuid; 93 | Key = key; 94 | Value = val; 95 | } 96 | 97 | /// 98 | /// Instantiate. 99 | /// 100 | /// GUID. 101 | /// Bucket GUID. 102 | /// Object GUID. 103 | /// Key. 104 | /// Value. 105 | public ObjectTag(string guid, string bucketGuid, string objectGuid, string key, string val) 106 | { 107 | if (String.IsNullOrEmpty(guid)) throw new ArgumentNullException(nameof(guid)); 108 | if (String.IsNullOrEmpty(bucketGuid)) throw new ArgumentNullException(nameof(bucketGuid)); 109 | if (String.IsNullOrEmpty(objectGuid)) throw new ArgumentNullException(nameof(objectGuid)); 110 | if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); 111 | 112 | GUID = guid; 113 | BucketGUID = bucketGuid; 114 | ObjectGUID = objectGuid; 115 | Key = key; 116 | Value = val; 117 | } 118 | 119 | #endregion 120 | 121 | #region Public-Methods 122 | 123 | #endregion 124 | 125 | #region Private-Methods 126 | 127 | #endregion 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Less3/Classes/RequestMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Classes 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using S3ServerLibrary; 7 | 8 | /// 9 | /// Less3 request metadata. 10 | /// 11 | public class RequestMetadata 12 | { 13 | #region Public-Members 14 | 15 | /// 16 | /// User. 17 | /// 18 | public User User = null; 19 | 20 | /// 21 | /// Credential. 22 | /// 23 | public Credential Credential = null; 24 | 25 | /// 26 | /// Bucket. 27 | /// 28 | public Bucket Bucket = null; 29 | 30 | /// 31 | /// Bucket client. 32 | /// 33 | internal BucketClient BucketClient = null; 34 | 35 | /// 36 | /// Bucket access control lists. 37 | /// 38 | public List BucketAcls = new List(); 39 | 40 | /// 41 | /// Bucket tags. 42 | /// 43 | public List BucketTags = new List(); 44 | 45 | /// 46 | /// Object. 47 | /// 48 | public Obj Obj = null; 49 | 50 | /// 51 | /// Object access control lists. 52 | /// 53 | public List ObjectAcls = new List(); 54 | 55 | /// 56 | /// Object tags. 57 | /// 58 | public List ObjectTags = new List(); 59 | 60 | /// 61 | /// Authentication result. 62 | /// 63 | public AuthenticationResult Authentication = AuthenticationResult.NotAuthenticated; 64 | 65 | /// 66 | /// Authorization result. 67 | /// 68 | public AuthorizationResult Authorization = AuthorizationResult.NotAuthorized; 69 | 70 | #endregion 71 | 72 | #region Private-Members 73 | 74 | #endregion 75 | 76 | #region Constructors-and-Factories 77 | 78 | /// 79 | /// Instantiate. 80 | /// 81 | public RequestMetadata() 82 | { 83 | 84 | } 85 | 86 | #endregion 87 | 88 | #region Public-Methods 89 | 90 | #endregion 91 | 92 | #region Private-Methods 93 | 94 | #endregion 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Less3/Classes/RetentionType.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Classes 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Runtime.Serialization; 6 | using System.Text; 7 | using System.Text.Json; 8 | using System.Text.Json.Serialization; 9 | 10 | /// 11 | /// Type of object retention. 12 | /// 13 | [JsonConverter(typeof(JsonStringEnumConverter))] 14 | public enum RetentionType 15 | { 16 | /// 17 | /// No retention specified. 18 | /// 19 | [EnumMember(Value = "NONE")] 20 | NONE, 21 | /// 22 | /// Governance retention. 23 | /// 24 | [EnumMember(Value = "GOVERNANCE")] 25 | GOVERNANCE, 26 | /// 27 | /// Compliance retention. 28 | /// 29 | [EnumMember(Value = "COMPLIANCE")] 30 | COMPLIANCE 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Less3/Classes/Settings.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Classes 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Net; 7 | using SyslogLogging; 8 | using DatabaseWrapper.Core; 9 | using Less3.Storage; 10 | using S3ServerLibrary; 11 | using Watson.ORM.Core; 12 | using WatsonWebserver.Core; 13 | 14 | /// 15 | /// Less3 settings. 16 | /// 17 | public class Settings 18 | { 19 | #region Public-Members 20 | 21 | /// 22 | /// Enable or disable the console. 23 | /// 24 | public bool EnableConsole { get; set; } = true; 25 | 26 | /// 27 | /// Enable or disable signature validation. 28 | /// 29 | public bool ValidateSignatures { get; set; } = true; 30 | 31 | /// 32 | /// Base domain, if using virtual hosted-style URLs, e.g. "localhost". 33 | /// 34 | public string BaseDomain { get; set; } = null; 35 | 36 | /// 37 | /// API key header for admin API requests. 38 | /// 39 | public string HeaderApiKey { get; set; } = "x-api-key"; 40 | 41 | /// 42 | /// Admin API key. 43 | /// 44 | public string AdminApiKey { get; set; } = "less3admin"; 45 | 46 | /// 47 | /// Region string. 48 | /// 49 | public string RegionString { get; set; } = "us-west-1"; 50 | 51 | /// 52 | /// Database settings. 53 | /// 54 | public DatabaseSettings Database { get; set; } = new DatabaseSettings("./less3.db"); 55 | 56 | /// 57 | /// Web server settings. 58 | /// 59 | public WebserverSettings Webserver { get; set; } = new WebserverSettings(); 60 | 61 | /// 62 | /// Storage settings. 63 | /// 64 | public SettingsStorage Storage { get; set; } = new SettingsStorage(); 65 | 66 | /// 67 | /// Logging settings. 68 | /// 69 | public SettingsLogging Logging { get; set; } = new SettingsLogging(); 70 | 71 | /// 72 | /// Debugging settings. 73 | /// 74 | public SettingsDebug Debug { get; set; } = new SettingsDebug(); 75 | 76 | #endregion 77 | 78 | #region Subordinate-Classes 79 | 80 | /// 81 | /// Storage settings. 82 | /// 83 | public class SettingsStorage 84 | { 85 | /// 86 | /// Temporary storage directory. 87 | /// 88 | public string TempDirectory { get; set; } = "./temp/"; 89 | 90 | /// 91 | /// Type of storage driver. 92 | /// 93 | public StorageDriverType StorageType { get; set; } = StorageDriverType.Disk; 94 | 95 | /// 96 | /// Storage directory for 'Disk' StorageType. 97 | /// 98 | public string DiskDirectory { get; set; } = "./disk/"; 99 | } 100 | 101 | /// 102 | /// Logging settings. 103 | /// 104 | public class SettingsLogging 105 | { 106 | /// 107 | /// IP address or hostname of the syslog server. 108 | /// 109 | public string SyslogServerIp { get; set; } = "127.0.0.1"; 110 | 111 | /// 112 | /// Syslog server port number. 113 | /// 114 | public int SyslogServerPort { get; set; } = 514; 115 | 116 | /// 117 | /// Minimum log level severity. 118 | /// 119 | public Severity MinimumLevel { get; set; } = Severity.Info; 120 | 121 | /// 122 | /// Enable or disable logging of HTTP requests. 123 | /// 124 | public bool LogHttpRequests { get; set; } = false; 125 | 126 | /// 127 | /// Enable or disable logging of S3 requests. 128 | /// 129 | public bool LogS3Requests { get; set; } = false; 130 | 131 | /// 132 | /// Enable or disable logging of exceptions. 133 | /// 134 | public bool LogExceptions { get; set; } = false; 135 | 136 | /// 137 | /// Enable or disable logging of signature validation. 138 | /// 139 | public bool LogSignatureValidation { get; set; } = false; 140 | 141 | /// 142 | /// Enable or disable logging to the console. 143 | /// 144 | public bool ConsoleLogging { get; set; } = true; 145 | 146 | /// 147 | /// Enable or disable logging to disk. 148 | /// 149 | public bool DiskLogging { get; set; } = true; 150 | 151 | /// 152 | /// Directory on disk to write log files. 153 | /// 154 | public string DiskDirectory { get; set; } = "./logs/"; 155 | } 156 | 157 | /// 158 | /// Debug settings. 159 | /// 160 | public class SettingsDebug 161 | { 162 | /// 163 | /// Enable or disable debugging of authentication logic. 164 | /// 165 | public bool Authentication { get; set; } = false; 166 | 167 | /// 168 | /// Enable or disable debugging of S3 request parsing. 169 | /// 170 | public bool S3Requests { get; set; } = false; 171 | 172 | /// 173 | /// Enable or disable debugging of exceptions. 174 | /// 175 | public bool Exceptions { get; set; } = false; 176 | } 177 | 178 | #endregion 179 | 180 | #region Constructors-and-Factories 181 | 182 | /// 183 | /// Instantiate. 184 | /// 185 | public Settings() 186 | { 187 | 188 | } 189 | 190 | /// 191 | /// Instantiate the object from a file. 192 | /// 193 | /// Filename. 194 | /// Settings. 195 | public static Settings FromFile(string filename) 196 | { 197 | if (String.IsNullOrEmpty(filename)) throw new ArgumentNullException(nameof(filename)); 198 | if (!File.Exists(filename)) throw new FileNotFoundException(nameof(filename)); 199 | 200 | string contents = Common.ReadTextFile(@filename); 201 | if (String.IsNullOrEmpty(contents)) 202 | { 203 | Common.ExitApplication("Settings", "Unable to read contents of " + filename, -1); 204 | return null; 205 | } 206 | 207 | Settings ret = null; 208 | 209 | try 210 | { 211 | ret = SerializationHelper.DeserializeJson(contents); 212 | if (ret == null) 213 | { 214 | Common.ExitApplication("Settings", "Unable to deserialize " + filename + " (null)", -1); 215 | return null; 216 | } 217 | } 218 | catch (Exception) 219 | { 220 | Common.ExitApplication("Settings", "Unable to deserialize " + filename + " (exception)", -1); 221 | return null; 222 | } 223 | 224 | return ret; 225 | } 226 | 227 | #endregion 228 | 229 | #region Internal-Methods 230 | 231 | #endregion 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/Less3/Classes/User.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Classes 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Data; 6 | using System.Text; 7 | using System.Text.Json; 8 | using System.Text.Json.Serialization; 9 | using Watson.ORM.Core; 10 | 11 | /// 12 | /// User object. 13 | /// 14 | [Table("users")] 15 | public class User 16 | { 17 | #region Public-Members 18 | 19 | /// 20 | /// Database identifier. 21 | /// 22 | [JsonIgnore] 23 | [Column("id", true, DataTypes.Int, false)] 24 | public int Id { get; set; } = 0; 25 | 26 | /// 27 | /// GUID. 28 | /// 29 | [Column("guid", false, DataTypes.Nvarchar, 64, false)] 30 | public string GUID { get; set; } = Guid.NewGuid().ToString(); 31 | 32 | /// 33 | /// Name of the user. 34 | /// 35 | [Column("name", false, DataTypes.Nvarchar, 256, false)] 36 | public string Name { get; set; } = null; 37 | 38 | /// 39 | /// Email address of the user. 40 | /// 41 | [Column("email", false, DataTypes.Nvarchar, 256, false)] 42 | public string Email { get; set; } = null; 43 | 44 | /// 45 | /// Timestamp from record creation, in UTC time. 46 | /// 47 | [Column("createdutc", false, DataTypes.DateTime, 6, 6, false)] 48 | public DateTime CreatedUtc { get; set; } = DateTime.Now.ToUniversalTime(); 49 | 50 | #endregion 51 | 52 | #region Private-Members 53 | 54 | #endregion 55 | 56 | #region Constructors-and-Factories 57 | 58 | /// 59 | /// Instantiate. 60 | /// 61 | public User() 62 | { 63 | 64 | } 65 | 66 | /// 67 | /// Instantiate. 68 | /// 69 | /// Name. 70 | /// Email. 71 | public User(string name, string email) 72 | { 73 | if (String.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); 74 | if (String.IsNullOrEmpty(email)) throw new ArgumentNullException(nameof(email)); 75 | 76 | GUID = Guid.NewGuid().ToString(); 77 | Name = name; 78 | Email = email; 79 | } 80 | 81 | /// 82 | /// Instantiate. 83 | /// 84 | /// GUID. 85 | /// Name. 86 | /// Email. 87 | public User(string guid, string name, string email) 88 | { 89 | if (String.IsNullOrEmpty(guid)) throw new ArgumentNullException(nameof(guid)); 90 | if (String.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); 91 | if (String.IsNullOrEmpty(email)) throw new ArgumentNullException(nameof(email)); 92 | 93 | GUID = guid; 94 | Name = name; 95 | Email = email; 96 | } 97 | 98 | #endregion 99 | 100 | #region Public-Methods 101 | 102 | #endregion 103 | 104 | #region Private-Methods 105 | 106 | #endregion 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Less3/Common.cs: -------------------------------------------------------------------------------- 1 | namespace Less3 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Data; 6 | using System.Dynamic; 7 | using System.IO; 8 | using System.IO.Compression; 9 | using System.Linq; 10 | using System.Security.AccessControl; 11 | using System.Security.Cryptography; 12 | using System.Security.Principal; 13 | using System.Text; 14 | using System.Text.Json; 15 | using System.Text.Json.Serialization; 16 | using System.Threading; 17 | using System.Threading.Tasks; 18 | using System.Xml; 19 | using System.Xml.Serialization; 20 | using XmlToPox; 21 | 22 | /// 23 | /// Common static methods. 24 | /// 25 | internal static class Common 26 | { 27 | #region Environment 28 | 29 | public static void ExitApplication(string method, string text, int returnCode) 30 | { 31 | Console.WriteLine("---"); 32 | Console.WriteLine(""); 33 | Console.WriteLine("The application has exited."); 34 | Console.WriteLine(""); 35 | Console.WriteLine(" Requested by : " + method); 36 | Console.WriteLine(" Reason text : " + text); 37 | Console.WriteLine(""); 38 | Console.WriteLine("---"); 39 | Environment.Exit(returnCode); 40 | return; 41 | } 42 | 43 | #endregion 44 | 45 | #region Directory 46 | 47 | public static List GetSubdirectoryList(string directory, bool recursive) 48 | { 49 | try 50 | { 51 | /* 52 | * Prepends the 'directory' variable to the name of each directory already 53 | * so each is immediately usable from the resultant list 54 | * 55 | * Does NOT append a slash 56 | * Does NOT include the original directory in the list 57 | * Does NOT include child files 58 | * 59 | * i.e. 60 | * C:\code\kvpbase 61 | * C:\code\kvpbase\src 62 | * C:\code\kvpbase\test 63 | * 64 | */ 65 | 66 | string[] folders; 67 | 68 | if (recursive) 69 | { 70 | folders = Directory.GetDirectories(@directory, "*", SearchOption.AllDirectories); 71 | } 72 | else 73 | { 74 | folders = Directory.GetDirectories(@directory, "*", SearchOption.TopDirectoryOnly); 75 | } 76 | 77 | List folderList = new List(); 78 | 79 | foreach (string folder in folders) 80 | { 81 | folderList.Add(folder); 82 | } 83 | 84 | return folderList; 85 | } 86 | catch (Exception) 87 | { 88 | return null; 89 | } 90 | } 91 | 92 | public static bool WalkDirectory( 93 | string environment, 94 | int depth, 95 | string directory, 96 | bool prependFilename, 97 | out List subdirectories, 98 | out List files, 99 | out long bytes, 100 | bool recursive) 101 | { 102 | subdirectories = new List(); 103 | files = new List(); 104 | bytes = 0; 105 | 106 | try 107 | { 108 | subdirectories = Common.GetSubdirectoryList(directory, false); 109 | files = Common.GetFileList(environment, directory, prependFilename); 110 | 111 | if (files != null && files.Count > 0) 112 | { 113 | foreach (String currFile in files) 114 | { 115 | FileInfo fi = new FileInfo(currFile); 116 | bytes += fi.Length; 117 | } 118 | } 119 | 120 | List queueSubdirectories = new List(); 121 | List queueFiles = new List(); 122 | long queueBytes = 0; 123 | 124 | if (recursive) 125 | { 126 | if (subdirectories == null || subdirectories.Count < 1) return true; 127 | depth += 2; 128 | 129 | foreach (string curr in subdirectories) 130 | { 131 | List childSubdirectories = new List(); 132 | List childFiles = new List(); 133 | long childBytes = 0; 134 | 135 | WalkDirectory( 136 | environment, 137 | depth, 138 | curr, 139 | prependFilename, 140 | out childSubdirectories, 141 | out childFiles, 142 | out childBytes, 143 | true); 144 | 145 | if (childSubdirectories != null) 146 | foreach (string childSubdir in childSubdirectories) 147 | queueSubdirectories.Add(childSubdir); 148 | 149 | if (childFiles != null) 150 | foreach (string childFile in childFiles) 151 | queueFiles.Add(childFile); 152 | 153 | queueBytes += childBytes; 154 | } 155 | } 156 | 157 | if (queueSubdirectories != null) 158 | foreach (string queueSubdir in queueSubdirectories) 159 | subdirectories.Add(queueSubdir); 160 | 161 | if (queueFiles != null) 162 | foreach (string queueFile in queueFiles) 163 | files.Add(queueFile); 164 | 165 | bytes += queueBytes; 166 | return true; 167 | } 168 | catch (Exception) 169 | { 170 | return false; 171 | } 172 | } 173 | 174 | public static bool DirectoryStatistics( 175 | DirectoryInfo dirinfo, 176 | bool recursive, 177 | out long bytes, 178 | out int files, 179 | out int subdirs) 180 | { 181 | bytes = 0; 182 | files = 0; 183 | subdirs = 0; 184 | 185 | try 186 | { 187 | FileInfo[] fis = dirinfo.GetFiles(); 188 | files = fis.Length; 189 | 190 | foreach (FileInfo fi in fis) 191 | { 192 | bytes += fi.Length; 193 | } 194 | 195 | // Add subdirectory sizes 196 | DirectoryInfo[] subdirinfos = dirinfo.GetDirectories(); 197 | 198 | if (recursive) 199 | { 200 | foreach (DirectoryInfo subdirinfo in subdirinfos) 201 | { 202 | subdirs++; 203 | long subdirBytes = 0; 204 | int subdirFiles = 0; 205 | int subdirSubdirectories = 0; 206 | 207 | if (Common.DirectoryStatistics(subdirinfo, recursive, out subdirBytes, out subdirFiles, out subdirSubdirectories)) 208 | { 209 | bytes += subdirBytes; 210 | files += subdirFiles; 211 | subdirs += subdirSubdirectories; 212 | } 213 | } 214 | } 215 | else 216 | { 217 | subdirs = subdirinfos.Length; 218 | } 219 | 220 | return true; 221 | } 222 | catch (Exception) 223 | { 224 | return false; 225 | } 226 | } 227 | 228 | #endregion 229 | 230 | #region File 231 | 232 | public static List GetFileList(string environment, string directory, bool prependFilename) 233 | { 234 | try 235 | { 236 | /* 237 | * 238 | * Returns only the filename unless prepend_filename is set 239 | * If prepend_filename is set, directory is prepended 240 | * 241 | */ 242 | 243 | DirectoryInfo info = new DirectoryInfo(directory); 244 | FileInfo[] files = info.GetFiles().OrderBy(p => p.CreationTime).ToArray(); 245 | List fileList = new List(); 246 | 247 | foreach (FileInfo file in files) 248 | { 249 | if (prependFilename) fileList.Add(directory + "/" + file.Name); 250 | else fileList.Add(file.Name); 251 | } 252 | 253 | return fileList; 254 | } 255 | catch (Exception) 256 | { 257 | return null; 258 | } 259 | } 260 | 261 | public static bool WriteFile(string filename, string content, bool append) 262 | { 263 | using (StreamWriter writer = new StreamWriter(filename, append)) 264 | { 265 | writer.WriteLine(content); 266 | } 267 | return true; 268 | } 269 | 270 | public static bool WriteFile(string filename, byte[] content) 271 | { 272 | if (content != null && content.Length > 0) 273 | { 274 | File.WriteAllBytes(filename, content); 275 | } 276 | else 277 | { 278 | File.Create(filename).Close(); 279 | } 280 | 281 | return true; 282 | } 283 | 284 | public static string ReadTextFile(string filename) 285 | { 286 | try 287 | { 288 | return File.ReadAllText(@filename); 289 | } 290 | catch (Exception) 291 | { 292 | return null; 293 | } 294 | } 295 | 296 | public static byte[] ReadBinaryFile(string filename) 297 | { 298 | try 299 | { 300 | return File.ReadAllBytes(@filename); 301 | } 302 | catch (Exception) 303 | { 304 | return null; 305 | } 306 | } 307 | 308 | #endregion 309 | 310 | #region Misc 311 | 312 | public static string Line(int count, string fill) 313 | { 314 | if (count < 1) return ""; 315 | 316 | string ret = ""; 317 | for (int i = 0; i < count; i++) 318 | { 319 | ret += fill; 320 | } 321 | 322 | return ret; 323 | } 324 | 325 | public static bool IsLaterThanNow(DateTime dt) 326 | { 327 | if (DateTime.Compare(dt, DateTime.Now) > 0) 328 | { 329 | return true; 330 | } 331 | else 332 | { 333 | return false; 334 | } 335 | } 336 | 337 | public static bool ContainsUnsafeCharacters(string data) 338 | { 339 | /* 340 | * 341 | * Returns true if unsafe characters exist 342 | * 343 | * 344 | */ 345 | 346 | // see https://kb.acronis.com/content/39790 347 | 348 | if (String.IsNullOrEmpty(data)) return false; 349 | if (data.Equals(".")) return true; 350 | if (data.Equals("..")) return true; 351 | 352 | if ( 353 | (String.Compare(data.ToLower(), "com1") == 0) || 354 | (String.Compare(data.ToLower(), "com2") == 0) || 355 | (String.Compare(data.ToLower(), "com3") == 0) || 356 | (String.Compare(data.ToLower(), "com4") == 0) || 357 | (String.Compare(data.ToLower(), "com5") == 0) || 358 | (String.Compare(data.ToLower(), "com6") == 0) || 359 | (String.Compare(data.ToLower(), "com7") == 0) || 360 | (String.Compare(data.ToLower(), "com8") == 0) || 361 | (String.Compare(data.ToLower(), "com9") == 0) || 362 | (String.Compare(data.ToLower(), "lpt1") == 0) || 363 | (String.Compare(data.ToLower(), "lpt2") == 0) || 364 | (String.Compare(data.ToLower(), "lpt3") == 0) || 365 | (String.Compare(data.ToLower(), "lpt4") == 0) || 366 | (String.Compare(data.ToLower(), "lpt5") == 0) || 367 | (String.Compare(data.ToLower(), "lpt6") == 0) || 368 | (String.Compare(data.ToLower(), "lpt7") == 0) || 369 | (String.Compare(data.ToLower(), "lpt8") == 0) || 370 | (String.Compare(data.ToLower(), "lpt9") == 0) || 371 | (String.Compare(data.ToLower(), "con") == 0) || 372 | (String.Compare(data.ToLower(), "nul") == 0) || 373 | (String.Compare(data.ToLower(), "prn") == 0) || 374 | (String.Compare(data.ToLower(), "con") == 0) 375 | ) 376 | { 377 | return true; 378 | } 379 | 380 | for (int i = 0; i < data.Length; i++) 381 | { 382 | if ( 383 | ((int)(data[i]) < 32) || // below range 384 | ((int)(data[i]) > 126) || // above range 385 | ((int)(data[i]) == 47) || // slash / 386 | ((int)(data[i]) == 92) || // backslash \ 387 | ((int)(data[i]) == 63) || // question mark ? 388 | ((int)(data[i]) == 60) || // less than < 389 | ((int)(data[i]) == 62) || // greater than > 390 | ((int)(data[i]) == 58) || // colon : 391 | ((int)(data[i]) == 42) || // asterisk * 392 | ((int)(data[i]) == 124) || // pipe | 393 | ((int)(data[i]) == 34) || // double quote " 394 | ((int)(data[i]) == 39) || // single quote ' 395 | ((int)(data[i]) == 94) // caret ^ 396 | ) 397 | { 398 | return true; 399 | } 400 | } 401 | 402 | return false; 403 | } 404 | 405 | #endregion 406 | 407 | #region Crypto 408 | 409 | public static byte[] Md5(byte[] data) 410 | { 411 | if (data == null) return null; 412 | return MD5.Create().ComputeHash(data); 413 | } 414 | 415 | public static byte[] Md5(Stream stream) 416 | { 417 | if (stream == null || !stream.CanRead) return null; 418 | 419 | MD5 md5 = MD5.Create(); 420 | return md5.ComputeHash(stream); 421 | } 422 | 423 | public static async Task Md5Async(Stream stream, int bufferSize) 424 | { 425 | using (var md5 = MD5.Create()) 426 | { 427 | byte[] buffer = new byte[bufferSize]; 428 | int read = 0; 429 | 430 | do 431 | { 432 | read = await stream.ReadAsync(buffer, 0, bufferSize); 433 | if (read > 0) 434 | { 435 | md5.TransformBlock(buffer, 0, read, null, 0); 436 | } 437 | } while (read > 0); 438 | 439 | md5.TransformFinalBlock(buffer, 0, 0); 440 | return md5.Hash; 441 | } 442 | } 443 | 444 | #endregion 445 | 446 | #region Encoding 447 | 448 | public static byte[] StreamToBytes(Stream input) 449 | { 450 | if (input == null) throw new ArgumentNullException(nameof(input)); 451 | if (!input.CanRead) throw new InvalidOperationException("Input stream is not readable"); 452 | 453 | byte[] buffer = new byte[16 * 1024]; 454 | using (MemoryStream ms = new MemoryStream()) 455 | { 456 | int read; 457 | 458 | while ((read = input.Read(buffer, 0, buffer.Length)) > 0) 459 | { 460 | ms.Write(buffer, 0, read); 461 | } 462 | 463 | return ms.ToArray(); 464 | } 465 | } 466 | 467 | public static string Base64ToString(string data) 468 | { 469 | if (String.IsNullOrEmpty(data)) return null; 470 | byte[] bytes = System.Convert.FromBase64String(data); 471 | return System.Text.UTF8Encoding.UTF8.GetString(bytes); 472 | } 473 | 474 | public static string StringToBase64(string data) 475 | { 476 | if (String.IsNullOrEmpty(data)) return null; 477 | byte[] bytes = System.Text.UTF8Encoding.UTF8.GetBytes(data); 478 | return System.Convert.ToBase64String(bytes); 479 | } 480 | 481 | public static string BytesToHexString(byte[] ba) 482 | { 483 | return BitConverter.ToString(ba).Replace("-", ""); 484 | } 485 | 486 | #endregion 487 | } 488 | } 489 | -------------------------------------------------------------------------------- /src/Less3/Dockerbuild.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | IF "%1" == "" GOTO :Usage 3 | IF "%2" == "" GOTO :Usage 4 | ECHO. 5 | ECHO Building for linux/amd64 and linux/arm64/v8... 6 | docker buildx build -f Dockerfile --platform linux/amd64,linux/arm64/v8 --tag jchristn/less3:%1 --load . 7 | 8 | IF "%2" NEQ "1" GOTO :Done 9 | 10 | ECHO. 11 | ECHO Pushing image... 12 | docker push jchristn/less3:%1 13 | 14 | GOTO :Done 15 | 16 | :Usage 17 | ECHO. 18 | ECHO Provide two arguments; the first being the version and the second being 1 or 0 to indicate whether or not to push. 19 | ECHO Example: dockerbuild.bat v2.0.0 1 20 | 21 | :Done 22 | ECHO. 23 | ECHO Done 24 | @ECHO ON 25 | -------------------------------------------------------------------------------- /src/Less3/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env 2 | WORKDIR /app 3 | 4 | # Copy everything from source directory into /App 5 | COPY . ./ 6 | EXPOSE 8000 7 | ENTRYPOINT ["dotnet", "Less3.dll"] 8 | -------------------------------------------------------------------------------- /src/Less3/Dockerrun.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | IF "%1" == "" GOTO :Usage 4 | 5 | if not exist system.json ( 6 | echo Configuration file system.json not found. 7 | exit /b 1 8 | ) 9 | 10 | REM Items that require persistence 11 | REM system.json 12 | REM less3.db 13 | REM logs/ 14 | REM temp/ 15 | 16 | REM Argument order matters! 17 | 18 | docker run ^ 19 | -p 8000:8000 ^ 20 | -t ^ 21 | -i ^ 22 | -e "TERM=xterm-256color" ^ 23 | -v .\system.json:/app/system.json ^ 24 | -v .\less3.db:/app/less3.db ^ 25 | -v .\logs\:/app/logs/ ^ 26 | -v .\temp\:/app/temp/ ^ 27 | jchristn/less3:%1 28 | 29 | GOTO :Done 30 | 31 | :Usage 32 | ECHO Provide one argument indicating the tag. 33 | ECHO Example: dockerrun.bat v2.1.11 34 | :Done 35 | @echo on 36 | -------------------------------------------------------------------------------- /src/Less3/Dockerrun.sh: -------------------------------------------------------------------------------- 1 | if [ -z "${IMG_TAG}" ]; then 2 | IMG_TAG='v2.1.11' 3 | fi 4 | 5 | echo Using image tag $IMG_TAG 6 | 7 | if [ ! -f "system.json" ] 8 | then 9 | echo Configuration file system.json not found. 10 | exit 11 | fi 12 | 13 | # Items that require persistence 14 | # system.json 15 | # less3.db 16 | # logs/ 17 | # temp/ 18 | 19 | # Argument order matters! 20 | 21 | docker run \ 22 | -p 8000:8000 \ 23 | -t \ 24 | -i \ 25 | -e "TERM=xterm-256color" \ 26 | -v ./system.json:/app/system.json \ 27 | -v ./less3.db:/app/less3.db \ 28 | -v ./logs/:/app/logs/ \ 29 | -v ./temp/:/app/temp/ \ 30 | jchristn/less3:$IMG_TAG 31 | -------------------------------------------------------------------------------- /src/Less3/LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/Less3/Less3.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netstandard2.0;netstandard2.1;net6.0;net8.0 6 | 2.1.13 7 | true 8 | Joel Christner 9 | Joel Christner 10 | Less3 11 | <3 Less3 is S3-compatible object storage that you can run on your laptop, server, or anywhere you like. 12 | (c)2024 Joel Christner 13 | 14 | https://github.com/jchristn/less3 15 | 16 | https://github.com/jchristn/less3 17 | Github 18 | S3 storage blob object storage rest restful 19 | Dependency update, validation with AWS CLI 20 | LICENSE.md 21 | favicon.png 22 | README.md 23 | heart.ico 24 | 25 | 26 | 27 | Less3.xml 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | Always 45 | 46 | 47 | Always 48 | 49 | 50 | Always 51 | 52 | 53 | Always 54 | 55 | 56 | Always 57 | 58 | 59 | Always 60 | 61 | 62 | True 63 | \ 64 | 65 | 66 | True 67 | 68 | 69 | 70 | True 71 | 72 | 73 | 74 | Always 75 | 76 | 77 | Always 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/Less3/Storage/DiskStorageDriver.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Storage 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Security.Cryptography; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Less3.Classes; 11 | 12 | /// 13 | /// Disk storage driver. 14 | /// 15 | public class DiskStorageDriver : StorageDriver 16 | { 17 | #region Public-Members 18 | 19 | /// 20 | /// Buffer size to use while reading file streams. 21 | /// 22 | public int StreamBufferSize 23 | { 24 | get 25 | { 26 | return _StreamBufferSize; 27 | } 28 | set 29 | { 30 | if (value < 1) throw new ArgumentException("Stream buffer size must be greater than zero bytes."); 31 | _StreamBufferSize = value; 32 | } 33 | } 34 | 35 | #endregion 36 | 37 | #region Private-Members 38 | 39 | private CancellationTokenSource _TokenSource = new CancellationTokenSource(); 40 | private CancellationToken _Token; 41 | private string _BaseDirectory = null; 42 | private int _StreamBufferSize = 65536; 43 | 44 | #endregion 45 | 46 | #region Constructors-and-Factories 47 | 48 | /// 49 | /// Instantiate. 50 | /// 51 | /// Base directory. 52 | public DiskStorageDriver(string baseDirectory) 53 | { 54 | if (String.IsNullOrEmpty(baseDirectory)) throw new ArgumentNullException(nameof(baseDirectory)); 55 | if (!Directory.Exists(baseDirectory)) Directory.CreateDirectory(baseDirectory); 56 | 57 | baseDirectory = baseDirectory.Replace('\\', '/'); 58 | if (!baseDirectory.EndsWith("/")) baseDirectory += "/"; 59 | 60 | _BaseDirectory = baseDirectory; 61 | _Token = _TokenSource.Token; 62 | } 63 | 64 | #endregion 65 | 66 | #region Public-Methods 67 | 68 | /// 69 | /// Delete an object by key. 70 | /// 71 | /// Key. 72 | public override void Delete(string key) 73 | { 74 | if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); 75 | string file = FilePath(key); 76 | if (File.Exists(file)) File.Delete(file); 77 | } 78 | 79 | /// 80 | /// Verify the existence of an object by key. 81 | /// 82 | /// Key. 83 | /// True if exists. 84 | public override bool Exists(string key) 85 | { 86 | if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); 87 | string file = FilePath(key); 88 | if (File.Exists(file)) return true; 89 | return false; 90 | } 91 | 92 | /// 93 | /// Read an object. 94 | /// 95 | /// Key. 96 | /// Data. 97 | public override byte[] Read(string key) 98 | { 99 | if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); 100 | string file = FilePath(key); 101 | return File.ReadAllBytes(file); 102 | } 103 | 104 | /// 105 | /// Read an object asynchronously. 106 | /// 107 | /// Key. 108 | /// Data. 109 | public override async Task ReadAsync(string key) 110 | { 111 | if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); 112 | string file = FilePath(key); 113 | 114 | using (FileStream fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true)) 115 | { 116 | var length = (int)fileStream.Length; 117 | var bytes = new byte[length]; 118 | await fileStream.ReadAsync(bytes, 0, length); 119 | return bytes; 120 | } 121 | } 122 | 123 | /// 124 | /// Read an object. 125 | /// Your code must close the stream when complete. 126 | /// 127 | /// Key. 128 | /// ObjectStream. 129 | public override ObjectStream ReadStream(string key) 130 | { 131 | if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); 132 | string file = FilePath(key); 133 | FileInfo fi = new FileInfo(file); 134 | long contentLength = fi.Length; 135 | FileStream stream = new FileStream(file, FileMode.Open); 136 | stream.Seek(0, SeekOrigin.Begin); 137 | return new ObjectStream(key, fi.Length, stream); 138 | } 139 | 140 | /// 141 | /// Read a specific number of bytes from a specific location in an object. 142 | /// 143 | /// Key. 144 | /// Starting position. 145 | /// Number of bytes to read. 146 | /// Data. 147 | public override byte[] ReadRange(string key, long indexStart, long count) 148 | { 149 | if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); 150 | if (indexStart < 0) throw new ArgumentException("Index start must be zero or greater."); 151 | if (count < 0) throw new ArgumentException("Count must be zero or greater."); 152 | 153 | string file = FilePath(key); 154 | FileInfo fi = new FileInfo(file); 155 | long contentLength = fi.Length; 156 | 157 | if (indexStart + count > contentLength) throw new ArgumentException("Index start combined with count must not result in a position that exceeds the size of the file."); 158 | 159 | using (FileStream fs = new FileStream(file, FileMode.Open)) 160 | { 161 | long bytesRemaining = count; 162 | int read = 0; 163 | byte[] buffer = null; 164 | 165 | using (MemoryStream ms = new MemoryStream()) 166 | { 167 | while (bytesRemaining > 0) 168 | { 169 | if (bytesRemaining > _StreamBufferSize) buffer = new byte[_StreamBufferSize]; 170 | else buffer = new byte[bytesRemaining]; 171 | 172 | read = fs.Read(buffer, 0, buffer.Length); 173 | if (read > 0) 174 | { 175 | ms.Write(buffer, 0, read); 176 | bytesRemaining -= read; 177 | } 178 | } 179 | 180 | return ms.ToArray(); 181 | } 182 | } 183 | } 184 | 185 | /// 186 | /// Read a specific number of bytes asynchronously from a specific location in an object. 187 | /// 188 | /// Key. 189 | /// Starting position. 190 | /// Number of bytes to read. 191 | /// Data. 192 | public override async Task ReadRangeAsync(string key, long indexStart, long count) 193 | { 194 | if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); 195 | if (indexStart < 0) throw new ArgumentException("Index start must be zero or greater."); 196 | if (count < 0) throw new ArgumentException("Count must be zero or greater."); 197 | 198 | string file = FilePath(key); 199 | FileInfo fi = new FileInfo(file); 200 | long contentLength = fi.Length; 201 | 202 | if (indexStart + count > contentLength) throw new ArgumentException("Index start combined with count must not result in a position that exceeds the size of the file."); 203 | 204 | using (FileStream fs = new FileStream(file, FileMode.Open)) 205 | { 206 | long bytesRemaining = count; 207 | int read = 0; 208 | byte[] buffer = null; 209 | 210 | using (MemoryStream ms = new MemoryStream()) 211 | { 212 | while (bytesRemaining > 0) 213 | { 214 | if (bytesRemaining > _StreamBufferSize) buffer = new byte[_StreamBufferSize]; 215 | else buffer = new byte[bytesRemaining]; 216 | 217 | read = await fs.ReadAsync(buffer, 0, buffer.Length); 218 | if (read > 0) 219 | { 220 | await ms.WriteAsync(buffer, 0, read); 221 | bytesRemaining -= read; 222 | } 223 | } 224 | 225 | return ms.ToArray(); 226 | } 227 | } 228 | } 229 | 230 | /// 231 | /// Read a specific number of bytes from a specific location in an object. 232 | /// Your code must close the stream when complete. 233 | /// 234 | /// Key. 235 | /// Starting position. 236 | /// Number of bytes to read. 237 | /// ObjectStream. 238 | public override ObjectStream ReadRangeStream(string key, long indexStart, long count) 239 | { 240 | if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); 241 | if (indexStart < 0) throw new ArgumentException("Index start must be zero or greater."); 242 | if (count < 0) throw new ArgumentException("Count must be zero or greater."); 243 | 244 | string file = FilePath(key); 245 | FileInfo fi = new FileInfo(file); 246 | long contentLength = fi.Length; 247 | 248 | if (indexStart + count > contentLength) throw new ArgumentException("Index start combined with count must not result in a position that exceeds the size of the file."); 249 | 250 | using (FileStream fs = new FileStream(file, FileMode.Open)) 251 | { 252 | long bytesRemaining = count; 253 | int read = 0; 254 | byte[] buffer = null; 255 | Stream ms = new MemoryStream(); 256 | 257 | while (bytesRemaining > 0) 258 | { 259 | if (bytesRemaining > _StreamBufferSize) buffer = new byte[_StreamBufferSize]; 260 | else buffer = new byte[bytesRemaining]; 261 | 262 | read = fs.Read(buffer, 0, buffer.Length); 263 | if (read > 0) 264 | { 265 | ms.Write(buffer, 0, read); 266 | bytesRemaining -= read; 267 | } 268 | } 269 | 270 | ms.Seek(0, SeekOrigin.Begin); 271 | return new ObjectStream(key, count, ms); 272 | } 273 | } 274 | 275 | /// 276 | /// Read a specific number of bytes asynchronously from a specific location in an object. 277 | /// Your code must close the stream when complete. 278 | /// 279 | /// Key. 280 | /// Starting position. 281 | /// Number of bytes to read. 282 | /// ObjectStream. 283 | public override async Task ReadRangeStreamAsync(string key, long indexStart, long count) 284 | { 285 | if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); 286 | if (indexStart < 0) throw new ArgumentException("Index start must be zero or greater."); 287 | if (count < 0) throw new ArgumentException("Count must be zero or greater."); 288 | 289 | string file = FilePath(key); 290 | FileInfo fi = new FileInfo(file); 291 | long contentLength = fi.Length; 292 | 293 | if (indexStart + count > contentLength) throw new ArgumentException("Index start combined with count must not result in a position that exceeds the size of the file."); 294 | 295 | using (FileStream fs = new FileStream(file, FileMode.Open)) 296 | { 297 | long bytesRemaining = count; 298 | int read = 0; 299 | byte[] buffer = null; 300 | Stream ms = new MemoryStream(); 301 | 302 | while (bytesRemaining > 0) 303 | { 304 | if (bytesRemaining > _StreamBufferSize) buffer = new byte[_StreamBufferSize]; 305 | else buffer = new byte[bytesRemaining]; 306 | 307 | read = await fs.ReadAsync(buffer, 0, buffer.Length); 308 | if (read > 0) 309 | { 310 | await ms.WriteAsync(buffer, 0, read); 311 | bytesRemaining -= read; 312 | } 313 | } 314 | 315 | ms.Seek(0, SeekOrigin.Begin); 316 | return new ObjectStream(key, count, ms); 317 | } 318 | } 319 | 320 | /// 321 | /// Write an object. 322 | /// 323 | /// Key. 324 | /// Data. 325 | /// MD5 hash. 326 | public override byte[] Write(string key, byte[] data) 327 | { 328 | if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); 329 | if (data == null) data = new byte[0]; 330 | MemoryStream ms = new MemoryStream(data); 331 | ms.Seek(0, SeekOrigin.Begin); 332 | return Write(key, data.Length, ms); 333 | } 334 | 335 | /// 336 | /// Write an object asynchronously. 337 | /// 338 | /// Key. 339 | /// Data. 340 | /// MD5 hash. 341 | public override async Task WriteAsync(string key, byte[] data) 342 | { 343 | if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); 344 | if (data == null) data = new byte[0]; 345 | MemoryStream ms = new MemoryStream(data); 346 | ms.Seek(0, SeekOrigin.Begin); 347 | return await WriteAsync(key, data.Length, ms); 348 | } 349 | 350 | /// 351 | /// Write an object. 352 | /// 353 | /// Key. 354 | /// Number of bytes to read from the stream. 355 | /// Stream. 356 | /// MD5 hash. 357 | public override byte[] Write(string key, long contentLength, Stream stream) 358 | { 359 | if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); 360 | 361 | string file = FilePath(key); 362 | using (FileStream fs = new FileStream(file, FileMode.Create)) 363 | { 364 | long bytesRemaining = contentLength; 365 | int read = 0; 366 | byte[] buffer = new byte[_StreamBufferSize]; 367 | 368 | while (bytesRemaining > 0) 369 | { 370 | read = stream.Read(buffer, 0, buffer.Length); 371 | if (read > 0) 372 | { 373 | fs.Write(buffer, 0, read); 374 | bytesRemaining -= read; 375 | } 376 | } 377 | } 378 | 379 | FileInfo fi = new FileInfo(file); 380 | 381 | using (FileStream fs = new FileStream(file, FileMode.Open)) 382 | { 383 | return Common.Md5(fs); 384 | } 385 | } 386 | 387 | /// 388 | /// Write an object asynchronously. 389 | /// 390 | /// Key. 391 | /// Number of bytes to read from the stream. 392 | /// Stream. 393 | /// MD5 hash. 394 | public override async Task WriteAsync(string key, long contentLength, Stream stream) 395 | { 396 | if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); 397 | 398 | string file = FilePath(key); 399 | using (FileStream fs = new FileStream(file, FileMode.Create)) 400 | { 401 | long bytesRemaining = contentLength; 402 | int read = 0; 403 | byte[] buffer = new byte[_StreamBufferSize]; 404 | 405 | while (bytesRemaining > 0) 406 | { 407 | read = await stream.ReadAsync(buffer, 0, buffer.Length); 408 | if (read > 0) 409 | { 410 | await fs.WriteAsync(buffer, 0, read); 411 | bytesRemaining -= read; 412 | } 413 | } 414 | } 415 | 416 | FileInfo fi = new FileInfo(file); 417 | 418 | using (FileStream fs = new FileStream(file, FileMode.Open)) 419 | { 420 | return await Common.Md5Async(fs, _StreamBufferSize); 421 | } 422 | } 423 | 424 | #endregion 425 | 426 | #region Private-Methods 427 | 428 | private string FilePath(string key) 429 | { 430 | return _BaseDirectory + key; 431 | } 432 | 433 | #endregion 434 | } 435 | } 436 | -------------------------------------------------------------------------------- /src/Less3/Storage/ObjectStream.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Storage 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Text; 7 | 8 | /// 9 | /// Stream of data for an object. 10 | /// 11 | public class ObjectStream 12 | { 13 | #region Public-Members 14 | 15 | /// 16 | /// Object key. 17 | /// 18 | public string Key { get; set; } 19 | 20 | /// 21 | /// Content length. 22 | /// 23 | public long ContentLength { get; set; } 24 | 25 | /// 26 | /// Stream containing data. 27 | /// 28 | public Stream Data { get; set; } 29 | 30 | #endregion 31 | 32 | #region Constructors-and-Factories 33 | 34 | /// 35 | /// Instantiate. 36 | /// 37 | /// Object key. 38 | /// Content length. 39 | /// Stream containing data. 40 | public ObjectStream(string key, long contentLength, Stream data) 41 | { 42 | if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); 43 | if (contentLength < 0) throw new ArgumentException("Content length must be zero or greater."); 44 | if (data == null) throw new ArgumentNullException(nameof(data)); 45 | 46 | Key = key; 47 | ContentLength = contentLength; 48 | Data = data; 49 | } 50 | 51 | #endregion 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Less3/Storage/StorageDriver.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Storage 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | /// 10 | /// Less3 storage driver; allows developers to build their own storage providers for Less3. 11 | /// 12 | public abstract class StorageDriver 13 | { 14 | #region Public-Methods 15 | 16 | /// 17 | /// Delete an object by key. 18 | /// 19 | /// Key. 20 | public abstract void Delete(string key); 21 | 22 | /// 23 | /// Verify the existence of an object by key. 24 | /// 25 | /// Key. 26 | /// True if exists. 27 | public abstract bool Exists(string key); 28 | 29 | /// 30 | /// Read an object. 31 | /// 32 | /// Key. 33 | /// Data. 34 | public abstract byte[] Read(string key); 35 | 36 | /// 37 | /// Read an object asynchronously. 38 | /// 39 | /// Key. 40 | /// Data. 41 | public abstract Task ReadAsync(string key); 42 | 43 | /// 44 | /// Read an object. 45 | /// 46 | /// Key. 47 | /// ObjectStream. 48 | public abstract ObjectStream ReadStream(string key); 49 | 50 | /// 51 | /// Read a specific number of bytes from a specific location in an object. 52 | /// 53 | /// Key. 54 | /// Starting position. 55 | /// Number of bytes to read. 56 | /// Data. 57 | public abstract byte[] ReadRange(string key, long indexStart, long count); 58 | 59 | /// 60 | /// Read a specific number of bytes asynchronously from a specific location in an object. 61 | /// 62 | /// Key. 63 | /// Starting position. 64 | /// Number of bytes to read. 65 | /// Data. 66 | public abstract Task ReadRangeAsync(string key, long indexStart, long count); 67 | 68 | /// 69 | /// Read a specific number of bytes from a specific location in an object. 70 | /// 71 | /// Key. 72 | /// Starting position. 73 | /// Number of bytes to read. 74 | /// ObjectStream. 75 | public abstract ObjectStream ReadRangeStream(string key, long indexStart, long count); 76 | 77 | /// 78 | /// Read a specific number of bytes asynchronously from a specific location in an object. 79 | /// 80 | /// Key. 81 | /// Starting position. 82 | /// Number of bytes to read. 83 | /// ObjectStream. 84 | public abstract Task ReadRangeStreamAsync(string key, long indexStart, long count); 85 | 86 | /// 87 | /// Write an object. 88 | /// 89 | /// Key. 90 | /// Data. 91 | /// MD5 hash. 92 | public abstract byte[] Write(string key, byte[] data); 93 | 94 | /// 95 | /// Write an object asynchronously. 96 | /// 97 | /// Key. 98 | /// Data. 99 | /// MD5 hash. 100 | public abstract Task WriteAsync(string key, byte[] data); 101 | 102 | /// 103 | /// Write an object. 104 | /// 105 | /// Key. 106 | /// Number of bytes to read from the stream. 107 | /// Stream. 108 | /// MD5 hash. 109 | public abstract byte[] Write(string key, long contentLength, Stream stream); 110 | 111 | /// 112 | /// Write an object asynchronously. 113 | /// 114 | /// Key. 115 | /// Number of bytes to read from the stream. 116 | /// Stream. 117 | /// MD5 hash. 118 | public abstract Task WriteAsync(string key, long contentLength, Stream stream); 119 | 120 | #endregion 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Less3/Storage/StorageDriverType.cs: -------------------------------------------------------------------------------- 1 | namespace Less3.Storage 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Runtime.Serialization; 6 | using System.Text; 7 | using System.Text.Json; 8 | using System.Text.Json.Serialization; 9 | 10 | /// 11 | /// Type of storage driver. 12 | /// 13 | [JsonConverter(typeof(JsonStringEnumConverter))] 14 | public enum StorageDriverType 15 | { 16 | /// 17 | /// Disk. 18 | /// 19 | [EnumMember(Value = "Disk")] 20 | Disk 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Less3/clean.bat: -------------------------------------------------------------------------------- 1 | del /q /f logs\ 2 | del /q /f temp\ 3 | del /q /f disk\ 4 | del /q /f system.json 5 | del /q /f less3.db 6 | -------------------------------------------------------------------------------- /src/Less3/clean.sh: -------------------------------------------------------------------------------- 1 | rm -rf logs\ 2 | rm -rf temp\ 3 | rm -rf disk\ 4 | rm -rf system.json 5 | rm -rf less3.db 6 | -------------------------------------------------------------------------------- /src/Less3/heart.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchristn/Less3/b973c87d9ffe0d1c988593c472d33924e74207d9/src/Less3/heart.ico --------------------------------------------------------------------------------