diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..b5c7d12 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,144 @@ +# ServiceStack mix GitHub Actions +`release.yml` generated from `x mix release-ecr-aws`, this template in designed to help with automating CI deployments to AWS ECS and dedicated AWS ECS cluster. +This is a cheap way to start without an AWS Application Load Balancer (ALB) and also be in a situation that will easier to add one once the web service needs additional scale or high availability. + +## Overview +`release.yml` is designed to work with a ServiceStack app templates deploying directly to a single server in a dedicated ECS cluster via templated GitHub Actions. + +## Setup +### Create unique ECS cluster +For this setup, it is best to create a separate cluster as cluster will only have the single instance in it running. +This pattern is to start from a good base with AWS ECS and automated CI deployment while avoiding the higher costs of needing to run an application load balancer (ALB). + +If/when you can justify the cost of an ALB for easier scaling and zero downtime deployment, the GitHub Action `release.yml` can be slightly modified to be used with a re-created or different ECS Service that is configured to be used with an Application Load Balancer and Target Group. + +### Elastic IP (optional) +The reason you might want to register this first is because we are only running one EC2 instance and hosting our own `nginx-proxy` on the same instance as the applications. +Since an `A` record will be pointing there, one advantage of not using an auto-assigned IP is that we can reassign the elastic IP if for what ever reason the instance goes down or is lost. + +## Launch to EC2 Instance +When launching the EC2 instance, you'll need to select an 'ECS optimized' AMI as the image used for your instance. +### Choose AMI +The easiest way to find the latest Amazon Linux 2 image for this is to go to the [AWS documentation for ECS-optimized AMIs and look up your region here](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-optimized_AMI.html#ecs-optimized-ami-linux). + +Using the AMI ID (starts with `ami-`) at the bottom, search in the 'Community AMIs' tab on the first step of the `Launch EC2 Instance` wizard. + +### Choose Instance Type +A t2.micro or larger will work fine, this pattern can be used to host multiple applications on the 1 server so if the number of applications gets larger, you might need a larger instance type. +> Note this pattern is suitable for testing prototypes or low traffic applications as it is cost effective and makes it easy to bundle multiple apps onto 1 EC2 instance. + +### Configure Instance +Under `IAM role`, use the `ecsInstanceRole`, if this is not available, see [AWS documentation for the process of checking if it exists and creating it if needed](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html). + +You will also want to add the following Userdata script (in the `Configure` step of the launch wizard) with your own `ECS_CLUSTER` value. This tells the ecs-agent running on the instance which ECS cluster the instance should join. + +```bash +#!/bin/bash +cat </etc/ecs/ecs.config +ECS_CLUSTER=rest-filess +ECS_AVAILABLE_LOGGING_DRIVERS=["awslogs", "syslog"] +ECS_ENABLE_CONTAINER_METADATA=true +EOS +``` + +Note down your cluster name as it will need to be used to create the cluster in ECS before it is visible. +See [`ECS Container Agent Configuration`](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-agent-config.html) for more information. + +### Add Storage +The default of 30gb is fine but take into account how large/how many applications you'll have running. + +### Configure Security Groups +You'll want to expose at least ports 80 and 443. + +### Setup Docker-compose and nginx-proxy +To let your server handle multiple ServiceStack applications and automate the generation and management of TLS certificates, an additional docker-compose file is provided via the `x mix` template, `nginx-proxy-compose.yml`. This docker-compose file is ready to run and can be copied to the deployment server. +> This is done via docker-compose rather than via ECS for simplicity. + +For example, once copied to remote `~/nginx-proxy-compose.yml`, the following command can be run on the remote server. + +``` +docker-compose -f ~/nginx-proxy-compose.yml up -d +``` + +This will run an nginx reverse proxy along with a companion container that will watch for additional containers in the same docker network and attempt to initialize them with valid TLS certificates. + +## GitHub Repository setup +The `release.yml` assumes 6 secrets have been setup. + +- AWS_ACCESS_KEY_ID - AWS access key for programmatic access to AWS APIs. +- AWS_SECRET_ACCESS_KEY - AWS access secrets for programmatic access to AWS APIs. +- AWS_REGION - default region for AWS API calls. +- AWS_ECS_CLUSTER - Cluster name in ECS, this should match the value in your Userdata. +- HOST_DOMAIN - Domain/submain of your application, eg `rest-files.example.com` . +- LETSENCRYPT_EMAIL - Email address, required for Let's Encrypt automated TLS certificates. + +These secrets are used to populate variables within GitHub Actions and other configuration files. + +For the AWS access, a separate user specifically for deploying via GitHub Actions should be used. + +The policies required for the complete initial setup will be: +- `AmazonEC2ContainerRegistryFullAccess` +- `AmazonECS_FullAccess` + +Once the application is successfully deployed the first time, reduced access for both ECR and ECS can be used instead. For application updates, the GitHub Action can use the following policy. + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": [ + "ecr:GetRegistryPolicy", + "ecr:PutImageTagMutability", + "ecr:GetDownloadUrlForLayer", + "ecr:DescribeRegistry", + "ecr:GetAuthorizationToken", + "ecr:ListTagsForResource", + "ecr:UploadLayerPart", + "ecr:ListImages", + "ecr:PutImage", + "ecr:UntagResource", + "ecr:BatchGetImage", + "ecr:CompleteLayerUpload", + "ecr:DescribeImages", + "ecr:TagResource", + "ecr:DescribeRepositories", + "ecr:InitiateLayerUpload", + "ecr:BatchCheckLayerAvailability", + "ecr:ReplicateImage", + "ecr:GetRepositoryPolicy", + "ecs:SubmitTaskStateChange", + "ecs:UpdateContainerInstancesState", + "ecs:RegisterContainerInstance", + "ecs:DescribeTaskDefinition", + "ecs:DescribeClusters", + "ecs:ListServices", + "ecs:UpdateService", + "ecs:ListTasks", + "ecs:ListTaskDefinitionFamilies", + "ecs:RegisterTaskDefinition", + "ecs:SubmitContainerStateChange", + "ecs:StopTask", + "ecs:DescribeServices", + "ecs:ListContainerInstances", + "ecs:DescribeContainerInstances", + "ecs:DeregisterContainerInstance", + "ecs:TagResource", + "ecs:DescribeTasks", + "ecs:UntagResource", + "ecs:ListTaskDefinitions", + "ecs:ListClusters" + ], + "Resource": "*" + } + ] +} +``` +> Further permission reduction can be done by reducing what resources can be accessed. +> Application permissions can be controlled via `taskRoleArn`, see [AWS docs for details](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html). + +## What's the process of the `release.yml`? + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/mix/release-ecr-aws-diagram.png) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..77560bf --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,35 @@ +name: Build + +on: + pull_request: {} + push: + branches: + - '**' # matches every branch + +jobs: + build: + runs-on: ubuntu-20.04 + steps: + - name: checkout + uses: actions/checkout@v2.0.0 + + - name: setup .net core + uses: actions/setup-dotnet@v1.7.2 + with: + dotnet-version: 5.0.100 + + - name: build + run: dotnet build + working-directory: ./src + + - name: test + run: | + dotnet test + if [ $? -eq 0 ]; then + echo TESTS PASSED + else + echo TESTS FAILED + exit 1 + fi + working-directory: ./src/RestFiles.Tests + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8bf8c5f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,94 @@ +name: Release +on: + release: + types: [published] +jobs: + push_to_ecr: + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: repository name fix + run: echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Login to Amazon ECR + id: login_ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Create ECR repo if not exists. + env: + ECR_REPOSITORY: ${{ env.image_repository_name }} + run: aws ecr describe-repositories --repository-names ${ECR_REPOSITORY} || aws ecr create-repository --repository-name ${ECR_REPOSITORY} + + - name: Build and push to ECR + id: push_image_to_ecr + uses: docker/build-push-action@v2.2.2 + with: + file: Dockerfile + context: . + push: true + tags: ${{ steps.login_ecr.outputs.registry }}/${{ env.image_repository_name }}:${{ github.event.release.tag_name }} + + deploy_ecs: + needs: push_to_ecr + runs-on: ubuntu-20.04 + steps: + - name: checkout + uses: actions/checkout@v2 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Login to Amazon ECR + id: login_ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Repository name fix and env values setup + run: | + echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV + echo "domain=${{ secrets.HOST_DOMAIN }}" >> $GITHUB_ENV + echo "letsencrypt_email=${{ secrets.LETSENCRYPT_EMAIL }}" >> $GITHUB_ENV + echo "app_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]' | cut -d'/' -f2)" >> $GITHUB_ENV + echo "cluster_name=${{ secrets.AWS_ECS_CLUSTER }}" >> $GITHUB_ENV + echo "image_url=${{ steps.login_ecr.outputs.registry }}/$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]'):${{ github.event.release.tag_name }}" >> $GITHUB_ENV + echo "aws_region=${{ secrets.AWS_REGION }}" >> $GITHUB_ENV + + - name: Populate task definition template + uses: danielr1996/envsubst-action@1.0.0 + env: + RELEASE_VERSION: ${{ github.event.release.tag_name }} + APP_NAME: ${{ env.app_name }} + IMAGE_URL: ${{ env.image_url }} + HOST_DOMAIN: ${{ env.domain }} + LETSENCRYPT_EMAIL: ${{ env.letsencrypt_email }} + AWS_REGION: ${{ env.aws_region }} + CLUSTER_NAME: ${{ env.cluster_name }} + with: + input: deploy/task-definition-template.json + output: deploy/task-definition.json + + - name: Create task definition if doesn't exist + run: aws ecs describe-task-definition --task-definition ${{ env.app_name }} || aws ecs register-task-definition --cli-input-json file://deploy/task-definition.json + + - name: Create ECS Service if not exists. + run: aws ecs describe-services --cluster ${{ env.cluster_name }} --services ${{ env.app_name }} | jq '.services[0]' -e || aws ecs create-service --cluster ${{ env.cluster_name }} --service-name ${{ env.app_name }} --task-definition ${{ env.app_name }} --desired-count 1 + + - name: Deploy new revision of the task definition + uses: aws-actions/amazon-ecs-deploy-task-definition@v1 + with: + task-definition: deploy/task-definition.json + service: ${{ env.app_name }} + cluster: ${{ env.cluster_name }} + force-new-deployment: true diff --git a/.gitignore b/.gitignore index aa60b74..5150791 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,40 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. +# Project specific +files/ + # User-specific files *.suo *.user +*.userosscache *.sln.docstates -.vs/ + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ +[Rr]eleases/ x64/ -build/ +x86/ bld/ [Bb]in/ [Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* -#NUNIT +# NUNIT *.VisualState.xml TestResult.xml @@ -30,6 +43,12 @@ TestResult.xml [Rr]eleasePS/ dlldata.c +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ +*.lock.json + *_i.c *_p.c *_i.h @@ -62,14 +81,18 @@ _Chutzpah* ipch/ *.aps *.ncb +*.opendb *.opensdf *.sdf *.cachefile +*.VC.db +*.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx +*.sap # TFS 2012 Local Workspace $tf/ @@ -82,7 +105,7 @@ _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user -# JustCode is a .NET coding addin-in +# JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in @@ -91,10 +114,14 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover +# Visual Studio code coverage results +*.coverage +*.coveragexml + # NCrunch -*.ncrunch* _NCrunch_* .*crunch*.local.xml +nCrunchTemp_* # MightyMoose *.mm.* @@ -122,41 +149,70 @@ publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml - -# NuGet Packages Directory -packages/ -## TODO: If the tool you use requires repositories.config uncomment the next line -#!packages/repositories.config - -# Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets -# This line needs to be after the ignore of the build folder (and the packages folder if the line above has been uncommented) -!packages/build/ - -# Windows Azure Build Output +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output csx/ *.build.csdef -# Windows Store app package directory +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ # Others -sql/ -*.Cache ClientBin/ -[Ss]tyle[Cc]op.* ~$* *~ *.dbmdl *.dbproj.schemaview +*.jfm *.pfx *.publishsettings node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ # RIA/Silverlight projects Generated_Code/ -# Backup & report files from converting an old project file to a newer -# Visual Studio version. Backup files are not needed, because we have git ;-) +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML @@ -173,3 +229,44 @@ UpgradeLog*.htm # Microsoft Fakes FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..14a3cdf --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,57 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/src/RestFiles/bin/Debug/netcoreapp2.0/RestFiles.dll", + "args": [], + "cwd": "${workspaceFolder}/src/RestFiles", + "console": "internalConsole", + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart" + }, + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/src/RestFiles/bin/Debug/netcoreapp2.0/RestFiles.dll", + "args": [], + "cwd": "${workspaceFolder}/src/RestFiles", + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart", + "launchBrowser": { + "enabled": true, + "args": "${auto-detect-url}", + "windows": { + "command": "cmd.exe", + "args": "/C start ${auto-detect-url}" + }, + "osx": { + "command": "open" + }, + "linux": { + "command": "xdg-open" + } + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/src/RestFiles/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..2d5fe37 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,20 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet build", + "options": { + "cwd": "${workspaceFolder}/src" + }, + "type": "shell", + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d71ebc4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build +WORKDIR /source + +COPY . . +RUN dotnet restore ./src + +WORKDIR /source/src +RUN dotnet publish -c release -o /app --no-restore + +FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS runtime +WORKDIR /app +COPY --from=build /app ./ +ENTRYPOINT ["dotnet", "RestFiles.dll"] diff --git a/README.md b/README.md index 7495bd0..15ac9ae 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -## [Rest Files](https://github.com/ServiceStackApps/RestFiles) +## [Rest Files](https://github.com/NetCoreApps/RestFiles) > GitHub-like browser with complete remote file management over REST APIs in 1 page of jQuery and 1 FileService.cs @@ -6,9 +6,9 @@ #### Features - - [1 Page jQuery](https://github.com/ServiceStackApps/RestFiles/blob/master/src/RestFiles/RestFiles/default.htm) - - [1 ServiceStack FilesService](https://github.com/ServiceStackApps/RestFiles/blob/master/src/RestFiles/RestFiles.ServiceInterface/FilesService.cs) + - [1 Page jQuery](https://github.com/NetCoreApps/RestFiles/blob/master/src/RestFiles/wwwroot/default.htm) + - [1 ServiceStack FilesService](https://github.com/NetCoreApps/RestFiles/blob/master/src/RestFiles.ServiceInterface/FilesService.cs) Try it out live at: [restfiles.servicestack.net](http://restfiles.servicestack.net) -Follow [@ServiceStack](http://twitter.com) or [+ServiceStack](https://plus.google.com/u/0/communities/112445368900682590445) for updates. +Follow [@ServiceStack](http://twitter.com) for updates. diff --git a/deploy/nginx-proxy-compose.yml b/deploy/nginx-proxy-compose.yml new file mode 100644 index 0000000..11ba1a9 --- /dev/null +++ b/deploy/nginx-proxy-compose.yml @@ -0,0 +1,45 @@ +version: '2' + +services: + nginx-proxy: + image: jwilder/nginx-proxy + container_name: nginx-proxy + restart: always + ports: + - "80:80" + - "443:443" + volumes: + - conf:/etc/nginx/conf.d + - vhost:/etc/nginx/vhost.d + - html:/usr/share/nginx/html + - dhparam:/etc/nginx/dhparam + - certs:/etc/nginx/certs:ro + - /var/run/docker.sock:/tmp/docker.sock:ro + network_mode: bridge + + letsencrypt: + image: jrcs/letsencrypt-nginx-proxy-companion + container_name: nginx-proxy-le + restart: always + environment: + - DEFAULT_EMAIL=you@example.com + volumes_from: + - nginx-proxy + volumes: + - certs:/etc/nginx/certs:rw + - acme:/etc/acme.sh + - /var/run/docker.sock:/var/run/docker.sock:ro + network_mode: bridge + +volumes: + conf: + vhost: + html: + dhparam: + certs: + acme: + +networks: + default: + external: + name: webproxy \ No newline at end of file diff --git a/deploy/task-definition-template.json b/deploy/task-definition-template.json new file mode 100644 index 0000000..c3797be --- /dev/null +++ b/deploy/task-definition-template.json @@ -0,0 +1,49 @@ +{ + "family": "${APP_NAME}", + "requiresCompatibilities": [ + "EC2" + ], + "networkMode": "bridge", + "containerDefinitions": [ + { + "portMappings": [ + { + "protocol": "tcp", + "containerPort": 5000 + } + ], + "environment": [ + { + "name": "VIRTUAL_HOST", + "value": "${HOST_DOMAIN}" + }, + { + "name": "LETSENCRYPT_HOST", + "value": "${HOST_DOMAIN}" + }, + { + "name": "LETSENCRYPT_EMAIL", + "value": "${LETSENCRYPT_EMAIL}" + }, + { + "name": "APP_VERSION", + "value": "${RELEASE_VERSION}" + } + ], + "mountPoints": [], + "memoryReservation": 128, + "volumesFrom": [], + "image": "${IMAGE_URL}", + "essential": true, + "name": "${APP_NAME}", + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "${CLUSTER_NAME}-${APP_NAME}", + "awslogs-region": "${AWS_REGION}", + "awslogs-create-group": "true" + } + } + } + ] +} diff --git a/src/NuGet.Config b/src/NuGet.Config new file mode 100644 index 0000000..c00d271 --- /dev/null +++ b/src/NuGet.Config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/RestFiles.ServiceInterface/AppConfig.cs b/src/RestFiles.ServiceInterface/AppConfig.cs new file mode 100644 index 0000000..219c606 --- /dev/null +++ b/src/RestFiles.ServiceInterface/AppConfig.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace RestFiles.ServiceInterface +{ + public class AppConfig + { + public string RootDirectory { get; set; } + + public List TextFileExtensions { get; set; } + + public List ExcludeDirectories { get; set; } + } +} \ No newline at end of file diff --git a/src/RestFiles.ServiceInterface/FilesService.cs b/src/RestFiles.ServiceInterface/FilesService.cs new file mode 100644 index 0000000..52cb97f --- /dev/null +++ b/src/RestFiles.ServiceInterface/FilesService.cs @@ -0,0 +1,157 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using RestFiles.ServiceModel; +using RestFiles.ServiceModel.Types; +using ServiceStack; +using ServiceStack.IO; + +namespace RestFiles.ServiceInterface +{ + /// + /// Define your ServiceStack web service request (i.e. Request DTO). + /// + public class FilesService : Service + { + public AppConfig Config { get; set; } + + public object Get(Files request) + { + var targetPath = GetAndValidateExistingPath(request); + + var isDirectory = VirtualFiles.IsDirectory(targetPath); + + if (!isDirectory && request.ForDownload) + return new HttpResult(VirtualFiles.GetFile(targetPath), asAttachment: true); + + var response = isDirectory + ? new FilesResponse { Directory = GetFolderResult(targetPath) } + : new FilesResponse { File = GetFileResult(targetPath) }; + + return response; + } + + public object Post(Files request) + { + var targetDir = GetPath(request); + + if (VirtualFiles.IsFile(targetDir)) + throw new NotSupportedException( + "POST only supports uploading new files. Use PUT to replace contents of an existing file"); + + if (!VirtualFiles.DirectoryExists(targetDir)) + { + VirtualFiles.WriteFile(targetDir.CombineWith(".temp"), ""); + VirtualFiles.DeleteFile(targetDir.CombineWith(".temp")); + } + + foreach (var uploadedFile in base.Request.Files) + { + var newFilePath = targetDir.CombineWith(uploadedFile.FileName); + VirtualFiles.WriteFile(newFilePath, uploadedFile.InputStream); + } + + return new FilesResponse(); + } + + public void Put(Files request) + { + var targetFile = VirtualFiles.GetFile(GetAndValidateExistingPath(request)); + + if (!Config.TextFileExtensions.Contains(targetFile.Extension)) + throw new NotSupportedException("PUT Can only update text files, not: " + targetFile.Extension); + + if (request.TextContents == null) + throw new ArgumentNullException("TextContents"); + + VirtualFiles.WriteFile(targetFile.VirtualPath, request.TextContents); + } + + public void Delete(Files request) + { + var targetFile = GetAndValidateExistingPath(request); + VirtualFiles.DeleteFile(targetFile); + } + + private FolderResult GetFolderResult(string targetPath) + { + var result = new FolderResult(); + + var dir = VirtualFiles.GetDirectory(targetPath); + var subFolders = dir.Directories.ToList(); + foreach (var subDir in subFolders) + { + if (Config.ExcludeDirectories.Contains(subDir.Name)) continue; + + result.Folders.Add(new Folder + { + Name = subDir.Name, + ModifiedDate = subDir.LastModified, + FileCount = subDir.GetFiles().Count(), + }); + } + + foreach (var fileInfo in dir.GetFiles()) + { + result.Files.Add(new ServiceModel.Types.File + { + Name = fileInfo.Name, + Extension = fileInfo.Extension, + FileSizeBytes = fileInfo.Length, + ModifiedDate = fileInfo.LastModified, + IsTextFile = Config.TextFileExtensions.Contains(fileInfo.Extension), + }); + } + + return result; + } + + private string GetPath(Files request) + { + return Config.RootDirectory.CombineWith(GetSafePath(request.Path)); + } + + private string GetAndValidateExistingPath(Files request) + { + var targetPath = GetPath(request); + if (!VirtualFiles.IsFile(targetPath) && !VirtualFiles.IsDirectory(targetPath)) + throw new HttpError(HttpStatusCode.NotFound, new FileNotFoundException("Could not find: " + request.Path)); + + return targetPath; + } + + private FileResult GetFileResult(string filePath) + { + var file = VirtualFiles.GetFile(filePath); + var isTextFile = Config.TextFileExtensions.Contains(file.Extension); + + return new FileResult + { + Name = file.Name, + Extension = file.Extension, + FileSizeBytes = file.Length, + IsTextFile = isTextFile, + Contents = isTextFile ? VirtualFiles.GetFile(file.VirtualPath).ReadAllText() : null, + ModifiedDate = file.LastModified, + }; + } + + public static string GetSafePath(string filePath) + { + if (string.IsNullOrEmpty(filePath)) return string.Empty; + + //Strip invalid chars + foreach (var invalidChar in Path.GetInvalidPathChars()) + { + filePath = filePath.Replace(invalidChar.ToString(), string.Empty); + } + + return filePath + .TrimStart('.', '/', '\\') //Remove illegal chars at the start + .Replace('\\', '/') //Switch all to use the same seperator + .Replace("../", string.Empty) //Remove access to top-level directories anywhere else + .Replace('/', Path.DirectorySeparatorChar); //Switch all to use the OS seperator + } + } +} \ No newline at end of file diff --git a/src/RestFiles.ServiceInterface/Properties/AssemblyInfo.cs b/src/RestFiles.ServiceInterface/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..3d90eb7 --- /dev/null +++ b/src/RestFiles.ServiceInterface/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("RestFiles.ServiceInterface")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("680d4b82-9c78-4601-83d8-0d11d7b288c7")] diff --git a/src/RestFiles.ServiceInterface/RestFiles.ServiceInterface.csproj b/src/RestFiles.ServiceInterface/RestFiles.ServiceInterface.csproj new file mode 100644 index 0000000..808d089 --- /dev/null +++ b/src/RestFiles.ServiceInterface/RestFiles.ServiceInterface.csproj @@ -0,0 +1,20 @@ + + + + netstandard2.0 + RestFiles.ServiceInterface + RestFiles.ServiceInterface + false + false + false + + + + + + + + + + + diff --git a/src/RestFiles.ServiceInterface/RevertFilesService.cs b/src/RestFiles.ServiceInterface/RevertFilesService.cs new file mode 100644 index 0000000..f38a6b2 --- /dev/null +++ b/src/RestFiles.ServiceInterface/RevertFilesService.cs @@ -0,0 +1,29 @@ +using RestFiles.ServiceModel; +using ServiceStack; +using ServiceStack.IO; + +namespace RestFiles.ServiceInterface +{ + /// + /// Define your ServiceStack web service request (i.e. Request DTO). + /// + public class RevertFilesService : Service + { + /// + /// Gets or sets the AppConfig. The built-in IoC used with ServiceStack autowires this property. + /// + public AppConfig Config { get; set; } + + public object Post(RevertFiles request) + { + VirtualFiles.DeleteFolder(Config.RootDirectory); + + foreach (var file in VirtualFiles.GetDirectory("wwwroot/src").GetAllMatchingFiles("*.*")) + { + VirtualFiles.WriteFile(file, file.VirtualPath.Replace("wwwroot/src/", Config.RootDirectory)); + } + + return new RevertFilesResponse(); + } + } +} \ No newline at end of file diff --git a/src/RestFiles/RestFiles.ServiceInterface/Support/FileExtensions.cs b/src/RestFiles.ServiceInterface/Support/FileExtensions.cs similarity index 100% rename from src/RestFiles/RestFiles.ServiceInterface/Support/FileExtensions.cs rename to src/RestFiles.ServiceInterface/Support/FileExtensions.cs diff --git a/src/RestFiles/RestFiles.ServiceModel/Files.cs b/src/RestFiles.ServiceModel/Files.cs similarity index 100% rename from src/RestFiles/RestFiles.ServiceModel/Files.cs rename to src/RestFiles.ServiceModel/Files.cs diff --git a/src/RestFiles.ServiceModel/Properties/AssemblyInfo.cs b/src/RestFiles.ServiceModel/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..5df10a8 --- /dev/null +++ b/src/RestFiles.ServiceModel/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("RestFiles.ServiceModel")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("f8a167d4-056f-4935-b76c-180d433eafd4")] diff --git a/src/RestFiles.ServiceModel/RestFiles.ServiceModel.csproj b/src/RestFiles.ServiceModel/RestFiles.ServiceModel.csproj new file mode 100644 index 0000000..6a8abaf --- /dev/null +++ b/src/RestFiles.ServiceModel/RestFiles.ServiceModel.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + RestFiles.ServiceModel + RestFiles.ServiceModel + false + false + false + + + + + + + diff --git a/src/RestFiles/RestFiles.ServiceModel/RevertFiles.cs b/src/RestFiles.ServiceModel/RevertFiles.cs similarity index 100% rename from src/RestFiles/RestFiles.ServiceModel/RevertFiles.cs rename to src/RestFiles.ServiceModel/RevertFiles.cs diff --git a/src/RestFiles.ServiceModel/Types/File.cs b/src/RestFiles.ServiceModel/Types/File.cs new file mode 100644 index 0000000..08e6318 --- /dev/null +++ b/src/RestFiles.ServiceModel/Types/File.cs @@ -0,0 +1,13 @@ +using System; + +namespace RestFiles.ServiceModel.Types +{ + public class File + { + public string Name { get; set; } + public string Extension { get; set; } + public long FileSizeBytes { get; set; } + public DateTime ModifiedDate { get; set; } + public bool IsTextFile { get; set; } + } +} \ No newline at end of file diff --git a/src/RestFiles.ServiceModel/Types/FileResult.cs b/src/RestFiles.ServiceModel/Types/FileResult.cs new file mode 100644 index 0000000..da113f5 --- /dev/null +++ b/src/RestFiles.ServiceModel/Types/FileResult.cs @@ -0,0 +1,14 @@ +using System; + +namespace RestFiles.ServiceModel.Types +{ + public class FileResult + { + public string Name { get; set; } + public string Extension { get; set; } + public long FileSizeBytes { get; set; } + public DateTime ModifiedDate { get; set; } + public bool IsTextFile { get; set; } + public string Contents { get; set; } + } +} \ No newline at end of file diff --git a/src/RestFiles.ServiceModel/Types/Folder.cs b/src/RestFiles.ServiceModel/Types/Folder.cs new file mode 100644 index 0000000..46a7580 --- /dev/null +++ b/src/RestFiles.ServiceModel/Types/Folder.cs @@ -0,0 +1,11 @@ +using System; + +namespace RestFiles.ServiceModel.Types +{ + public class Folder + { + public string Name { get; set; } + public DateTime ModifiedDate { get; set; } + public int FileCount { get; set; } + } +} \ No newline at end of file diff --git a/src/RestFiles.ServiceModel/Types/FolderResult.cs b/src/RestFiles.ServiceModel/Types/FolderResult.cs new file mode 100644 index 0000000..a8f9247 --- /dev/null +++ b/src/RestFiles.ServiceModel/Types/FolderResult.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace RestFiles.ServiceModel.Types +{ + public class FolderResult + { + public FolderResult() + { + Folders = new List(); + Files = new List(); + } + + public List Folders { get; set; } + public List Files { get; set; } + } +} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/src/RestFiles.Tests/AsyncRestClientTests.cs b/src/RestFiles.Tests/AsyncRestClientTests.cs similarity index 86% rename from src/RestFiles/RestFiles/App_Data/src/RestFiles.Tests/AsyncRestClientTests.cs rename to src/RestFiles.Tests/AsyncRestClientTests.cs index 7ac23c4..bfe195a 100644 --- a/src/RestFiles/RestFiles/App_Data/src/RestFiles.Tests/AsyncRestClientTests.cs +++ b/src/RestFiles.Tests/AsyncRestClientTests.cs @@ -27,26 +27,24 @@ public class AsyncRestClientTests private const string TestUploadFileContents = "THIS FILE IS USED FOR UPLOADING IN TESTS"; public string FilesRootDir; - RestFilesHttpListener appHost; + TestAppHost appHost; - [TestFixtureSetUp] + [OneTimeSetUp] public void TextFixtureSetUp() { - appHost = new RestFilesHttpListener(); + appHost = new TestAppHost(); appHost.Init(); + appHost.Start(TestAppHost.ListeningOn); } - [TestFixtureTearDown] - public void TestFixtureTearDown() - { - if (appHost != null) appHost.Dispose(); - appHost = null; - } + [OneTimeTearDown] + public void TestFixtureTearDown() => appHost.Dispose(); [SetUp] public void OnBeforeEachTest() { - FilesRootDir = appHost.Config.RootDirectory; + //Setup the files directory with some test files and folders + FilesRootDir = "App_Data/files/"; if (Directory.Exists(FilesRootDir)) { Directory.Delete(FilesRootDir, true); @@ -57,11 +55,9 @@ public void OnBeforeEachTest() File.WriteAllText(Path.Combine(FilesRootDir, "TESTUPLOAD.txt"), TestUploadFileContents); } - public IRestClientAsync CreateAsyncRestClient() + public JsonServiceClient CreateAsyncRestClient() { return new JsonServiceClient(WebServiceHostUrl); //Best choice for Ajax web apps, faster than XML - //return new XmlServiceClient(WebServiceHostUrl); //Ubiquitous structured data format best for supporting non .NET clients - //return new JsvServiceClient(WebServiceHostUrl); //Fastest, most compact and resilient format great for .NET to .NET client / server } private static void FailOnAsyncError(T response, Exception ex) @@ -90,23 +86,17 @@ public async Task Can_GetAsync_to_retrieve_existing_folder_listing() Assert.That(response.Directory.Files.Count, Is.EqualTo(2)); } - [Test] - public async Task Can_PostAsync_to_path_without_uploaded_files_to_create_a_new_Directory() - { - var restClient = CreateAsyncRestClient(); - - FilesResponse response = await restClient.PostAsync("files/SubFolder/NewFolder", new Files()); - - Assert.That(Directory.Exists(FilesRootDir + "SubFolder/NewFolder")); - } - [Test] public void Can_WebRequest_POST_upload_file_to_save_new_file_and_create_new_Directory() { var webRequest = WebRequest.Create(WebServiceHostUrl + "files/UploadedFiles/"); var fileToUpload = new FileInfo(FilesRootDir + "TESTUPLOAD.txt"); - webRequest.UploadFile(fileToUpload, MimeTypes.GetMimeType(fileToUpload.Name)); + using (var stream = fileToUpload.OpenRead()) + { + webRequest.UploadFile(stream, fileToUpload.Name); + var webRes = PclExport.Instance.GetResponse(webRequest); + } Assert.That(Directory.Exists(FilesRootDir + "UploadedFiles")); Assert.That(File.ReadAllText(FilesRootDir + "UploadedFiles/TESTUPLOAD.txt"), @@ -172,7 +162,7 @@ public async Task GET_a_file_that_doesnt_exist_throws_a_404_FileNotFoundExceptio } [Test] - public async Task POST_to_an_existing_file_throws_a_500_NotSupportedException() + public void POST_to_an_existing_file_throws_a_500_NotSupportedException() { var restClient = (IRestClient)CreateAsyncRestClient(); diff --git a/src/RestFiles.Tests/RestFiles.Tests.csproj b/src/RestFiles.Tests/RestFiles.Tests.csproj new file mode 100644 index 0000000..2a1dd32 --- /dev/null +++ b/src/RestFiles.Tests/RestFiles.Tests.csproj @@ -0,0 +1,23 @@ + + + + net5 + RestFiles.Tests + RestFiles.Tests + Library + + + + + + + + + + + + + + + + diff --git a/src/RestFiles.Tests/SyncRestClientTests.cs b/src/RestFiles.Tests/SyncRestClientTests.cs new file mode 100644 index 0000000..e5f8e31 --- /dev/null +++ b/src/RestFiles.Tests/SyncRestClientTests.cs @@ -0,0 +1,228 @@ +using System; +using System.IO; +using System.Linq; +using Funq; +using NUnit.Framework; +using RestFiles.ServiceInterface; +using RestFiles.ServiceModel; +using ServiceStack; +using File = System.IO.File; + +/* For syntax highlighting and better readability of this file, view it on GitHub: + * https://github.com/ServiceStack/ServiceStack.Examples/blob/master/src/RestFiles/RestFiles.Tests/SyncRestClientTests.cs + */ + +namespace RestFiles.Tests +{ + /// + /// These test show how you can call ServiceStack REST web services synchronously using an IRestClient. + /// + /// Sync IO calls are the most familiar calling convention for .NET developers as it provides the simplest + /// API to use and requires the least code and effort as it allows you to program sequentially + /// + [TestFixture] + public class SyncRestClientTests + { + public const string WebServiceHostUrl = "http://localhost:8080/"; + private const string ReadmeFileContents = "THIS IS A README FILE"; + private const string ReplacedFileContents = "THIS README FILE HAS BEEN REPLACED"; + private const string TestUploadFileContents = "THIS FILE IS USED FOR UPLOADING IN TESTS"; + public string FilesRootDir; + + TestAppHost appHost; + + [OneTimeSetUp] + public void TextFixtureSetUp() + { + appHost = new TestAppHost(); + appHost.Init(); + appHost.Start(TestAppHost.ListeningOn); + } + + [OneTimeTearDown] + public void TestFixtureTearDown() => appHost.Dispose(); + + [SetUp] + public void OnBeforeEachTest() + { + //Setup the files directory with some test files and folders + FilesRootDir = "App_Data/files/"; + if (Directory.Exists(FilesRootDir)) + { + Directory.Delete(FilesRootDir, true); + } + Directory.CreateDirectory(FilesRootDir + "SubFolder"); + Directory.CreateDirectory(FilesRootDir + "SubFolder2"); + File.WriteAllText(Path.Combine(FilesRootDir, "README.txt"), ReadmeFileContents); + File.WriteAllText(Path.Combine(FilesRootDir, "TESTUPLOAD.txt"), TestUploadFileContents); + } + + /// + /// Choose your favourite format to run tests with + /// + public IRestClient CreateRestClient() + { + return new JsonServiceClient(WebServiceHostUrl); //Best choice for Ajax web apps, 3x faster than XML + //return new XmlServiceClient(WebServiceHostUrl); //Ubiquitous structured data format best for supporting non .NET clients + //return new JsvServiceClient(WebServiceHostUrl); //Fastest, most compact and resilient format great for .NET to .NET client > server + } + + [Test] + public void Can_Get_to_retrieve_existing_file() + { + var restClient = CreateRestClient(); + + var response = restClient.Get("files/README.txt"); + + Assert.That(response.File.Contents, Is.EqualTo("THIS IS A README FILE")); + } + + [Test] + public void Can_Get_to_retrieve_existing_folder_listing() + { + var restClient = CreateRestClient(); + + var response = restClient.Get("files/"); + + Assert.That(response.Directory.Folders.Count, Is.EqualTo(2)); + Assert.That(response.Directory.Files.Count, Is.EqualTo(2)); + } + + [Test] + public void Can_Post_to_path_without_uploaded_files_to_create_a_new_Directory() + { + var restClient = CreateRestClient(); + + var response = restClient.Post("files/SubFolder/NewFolder", new Files()); + + Assert.That(Directory.Exists(FilesRootDir + "SubFolder/NewFolder")); + } + + [Test] + public void Can_WebRequest_POST_upload_file_to_save_new_file_and_create_new_Directory() + { + var restClient = CreateRestClient(); + + var fileToUpload = new FileInfo(FilesRootDir + "TESTUPLOAD.txt"); + + var response = restClient.PostFile("files/UploadedFiles/", + fileToUpload, MimeTypes.GetMimeType(fileToUpload.Name)); + + Assert.That(Directory.Exists(FilesRootDir + "UploadedFiles")); + Assert.That(File.ReadAllText(FilesRootDir + "UploadedFiles/TESTUPLOAD.txt"), + Is.EqualTo(TestUploadFileContents)); + } + + [Test] + public void Can_Put_to_replace_text_content_of_an_existing_file() + { + var restClient = CreateRestClient(); + + var response = restClient.Put(WebServiceHostUrl + "files/README.txt", + new Files { TextContents = ReplacedFileContents }); + + Assert.That(File.ReadAllText(FilesRootDir + "README.txt"), + Is.EqualTo(ReplacedFileContents)); + } + + [Test] + public void Can_Delete_to_replace_text_content_of_an_existing_file() + { + var restClient = CreateRestClient(); + + var response = restClient.Delete("files/README.txt"); + + Assert.That(!File.Exists(FilesRootDir + "README.txt")); + } + + + /* + * Error Handling Tests + */ + [Test] + public void GET_a_file_that_doesnt_exist_throws_a_404_FileNotFoundException() + { + var restClient = CreateRestClient(); + + try + { + var response = restClient.Get(WebServiceHostUrl + "files/UnknownFolder"); + + Assert.Fail("Should fail with 404 FileNotFoundException"); + } + catch (WebServiceException webEx) + { + Assert.That(webEx.StatusCode, Is.EqualTo(404)); + var response = (FilesResponse)webEx.ResponseDto; + Assert.That(response.ResponseStatus.ErrorCode, Is.EqualTo(typeof(FileNotFoundException).Name)); + Assert.That(response.ResponseStatus.Message, Is.EqualTo("Could not find: UnknownFolder")); + } + } + + [Test] + public void POST_to_an_existing_file_throws_a_500_NotSupportedException() + { + var restClient = CreateRestClient(); + + var fileToUpload = new FileInfo(FilesRootDir + "TESTUPLOAD.txt"); + + try + { + var response = restClient.PostFile(WebServiceHostUrl + "files/README.txt", + fileToUpload, MimeTypes.GetMimeType(fileToUpload.Name)); + + Assert.Fail("Should fail with NotSupportedException"); + } + catch (WebServiceException webEx) + { + Assert.That(webEx.StatusCode, Is.EqualTo(405)); + var response = (FilesResponse)webEx.ResponseDto; + Assert.That(response.ResponseStatus.ErrorCode, Is.EqualTo(typeof(NotSupportedException).Name)); + Assert.That(response.ResponseStatus.Message, + Is.EqualTo("POST only supports uploading new files. Use PUT to replace contents of an existing file")); + } + } + + [Test] + public void PUT_to_replace_a_non_existing_file_throws_404() + { + var restClient = CreateRestClient(); + + try + { + var response = restClient.Put(WebServiceHostUrl + "files/non-existing-file.txt", + new Files { TextContents = ReplacedFileContents }); + + Assert.Fail("Should fail with 404 FileNotFoundException"); + } + catch (WebServiceException webEx) + { + Assert.That(webEx.StatusCode, Is.EqualTo(404)); + var response = (FilesResponse)webEx.ResponseDto; + Assert.That(response.ResponseStatus.ErrorCode, Is.EqualTo(typeof(FileNotFoundException).Name)); + Assert.That(response.ResponseStatus.Message, Is.EqualTo("Could not find: non-existing-file.txt")); + } + } + + [Test] + public void DELETE_a_non_existing_file_throws_404() + { + var restClient = CreateRestClient(); + + try + { + var response = restClient.Delete(WebServiceHostUrl + "files/non-existing-file.txt"); + + Assert.Fail("Should fail with 404 FileNotFoundException"); + } + catch (WebServiceException webEx) + { + Assert.That(webEx.StatusCode, Is.EqualTo(404)); + var response = (FilesResponse)webEx.ResponseDto; + Assert.That(response.ResponseStatus.ErrorCode, Is.EqualTo(typeof(FileNotFoundException).Name)); + Assert.That(response.ResponseStatus.Message, Is.EqualTo("Could not find: non-existing-file.txt")); + } + } + + } +} \ No newline at end of file diff --git a/src/RestFiles.Tests/TestAppHost.cs b/src/RestFiles.Tests/TestAppHost.cs new file mode 100644 index 0000000..45c18c9 --- /dev/null +++ b/src/RestFiles.Tests/TestAppHost.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; +using Funq; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using RestFiles.ServiceInterface; +using ServiceStack; + +namespace RestFiles.Tests +{ + public class TestAppHost + : AppSelfHostBase + { + public const string ListeningOn = "http://localhost:8080/"; + + public TestAppHost() + : base("Self Host Integration Tests", typeof(FilesService).Assembly) { } + + public AppConfig AppConfig { get; set; } + + public override void Configure(Container container) + { + this.AppConfig = new AppConfig + { + RootDirectory = "App_Data/files/", + TextFileExtensions = "txt,sln,proj,cs,config,asax".Split(',').ToList(), + ExcludeDirectories = new List(), + }; + container.Register(this.AppConfig); + } + } +} diff --git a/src/RestFiles.sln b/src/RestFiles.sln new file mode 100644 index 0000000..2e356ea --- /dev/null +++ b/src/RestFiles.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26114.2 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestFiles", "RestFiles\RestFiles.csproj", "{7FD3201E-FD63-4809-A22B-E2614F126D68}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestFiles.ServiceInterface", "RestFiles.ServiceInterface\RestFiles.ServiceInterface.csproj", "{680D4B82-9C78-4601-83D8-0D11D7B288C7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestFiles.ServiceModel", "RestFiles.ServiceModel\RestFiles.ServiceModel.csproj", "{F8A167D4-056F-4935-B76C-180D433EAFD4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestFiles.Tests", "RestFiles.Tests\RestFiles.Tests.csproj", "{D33DF484-D242-457E-AD8C-4C192B16EF9E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7FD3201E-FD63-4809-A22B-E2614F126D68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7FD3201E-FD63-4809-A22B-E2614F126D68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7FD3201E-FD63-4809-A22B-E2614F126D68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7FD3201E-FD63-4809-A22B-E2614F126D68}.Release|Any CPU.Build.0 = Release|Any CPU + {680D4B82-9C78-4601-83D8-0D11D7B288C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {680D4B82-9C78-4601-83D8-0D11D7B288C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {680D4B82-9C78-4601-83D8-0D11D7B288C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {680D4B82-9C78-4601-83D8-0D11D7B288C7}.Release|Any CPU.Build.0 = Release|Any CPU + {F8A167D4-056F-4935-B76C-180D433EAFD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8A167D4-056F-4935-B76C-180D433EAFD4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8A167D4-056F-4935-B76C-180D433EAFD4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8A167D4-056F-4935-B76C-180D433EAFD4}.Release|Any CPU.Build.0 = Release|Any CPU + {D33DF484-D242-457E-AD8C-4C192B16EF9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D33DF484-D242-457E-AD8C-4C192B16EF9E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D33DF484-D242-457E-AD8C-4C192B16EF9E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D33DF484-D242-457E-AD8C-4C192B16EF9E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/RestFiles/Program.cs b/src/RestFiles/Program.cs new file mode 100644 index 0000000..e17225d --- /dev/null +++ b/src/RestFiles/Program.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; + +namespace RestFiles +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .UseUrls("http://0.0.0.0:5000/") + .Build(); + } +} diff --git a/src/RestFiles/Properties/launchSettings.json b/src/RestFiles/Properties/launchSettings.json new file mode 100644 index 0000000..957c140 --- /dev/null +++ b/src/RestFiles/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:50025/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "RestFiles": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/src/RestFiles/RestFiles.ServiceInterface/AppConfig.cs b/src/RestFiles/RestFiles.ServiceInterface/AppConfig.cs deleted file mode 100644 index d97082d..0000000 --- a/src/RestFiles/RestFiles.ServiceInterface/AppConfig.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using ServiceStack; -using ServiceStack.Configuration; - -namespace RestFiles.ServiceInterface -{ - public class AppConfig - { - public AppConfig() - { - this.TextFileExtensions = new List(); - this.ExcludeDirectories = new List(); - } - - public AppConfig(IAppSettings resources) - { - this.RootDirectory = resources.GetString("RootDirectory").MapHostAbsolutePath() - .Replace('\\', Path.DirectorySeparatorChar); - - this.TextFileExtensions = resources.GetList("TextFileExtensions"); - this.ExcludeDirectories = resources.GetList("ExcludeDirectories"); - } - - public string RootDirectory { get; set; } - - public IList TextFileExtensions { get; set; } - - public IList ExcludeDirectories { get; set; } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles.ServiceInterface/FilesService.cs b/src/RestFiles/RestFiles.ServiceInterface/FilesService.cs deleted file mode 100644 index db4fca7..0000000 --- a/src/RestFiles/RestFiles.ServiceInterface/FilesService.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System; -using System.IO; -using System.Net; -using RestFiles.ServiceInterface.Support; -using RestFiles.ServiceModel; -using RestFiles.ServiceModel.Types; -using ServiceStack; -using File = System.IO.File; - -namespace RestFiles.ServiceInterface -{ - /// - /// Define your ServiceStack web service request (i.e. Request DTO). - /// - public class FilesService : Service - { - /// - /// Gets or sets the AppConfig. The built-in IoC used with ServiceStack autowires this property. - /// - public AppConfig Config { get; set; } - - public object Get(Files request) - { - var targetFile = GetAndValidateExistingPath(request); - - var isDirectory = Directory.Exists(targetFile.FullName); - - if (!isDirectory && request.ForDownload) - return new HttpResult(targetFile, asAttachment: true); - - var response = isDirectory - ? new FilesResponse { Directory = GetFolderResult(targetFile.FullName) } - : new FilesResponse { File = GetFileResult(targetFile) }; - - return response; - } - - public object Post(Files request) - { - var targetDir = GetPath(request); - - var isExistingFile = targetDir.Exists - && (targetDir.Attributes & FileAttributes.Directory) != FileAttributes.Directory; - - if (isExistingFile) - throw new NotSupportedException( - "POST only supports uploading new files. Use PUT to replace contents of an existing file"); - - if (!Directory.Exists(targetDir.FullName)) - Directory.CreateDirectory(targetDir.FullName); - - foreach (var uploadedFile in base.Request.Files) - { - var newFilePath = Path.Combine(targetDir.FullName, uploadedFile.FileName); - uploadedFile.SaveTo(newFilePath); - } - - return new FilesResponse(); - } - - public void Put(Files request) - { - var targetFile = GetAndValidateExistingPath(request); - - if (!this.Config.TextFileExtensions.Contains(targetFile.Extension)) - throw new NotSupportedException("PUT Can only update text files, not: " + targetFile.Extension); - - if (request.TextContents == null) - throw new ArgumentNullException("TextContents"); - - File.WriteAllText(targetFile.FullName, request.TextContents); - } - - public void Delete(Files request) - { - var targetFile = GetAndValidateExistingPath(request); - File.Delete(targetFile.FullName); - } - - private FolderResult GetFolderResult(string targetPath) - { - var result = new FolderResult(); - - foreach (var dirPath in Directory.GetDirectories(targetPath)) - { - var dirInfo = new DirectoryInfo(dirPath); - - if (this.Config.ExcludeDirectories.Contains(dirInfo.Name)) continue; - - result.Folders.Add(new Folder - { - Name = dirInfo.Name, - ModifiedDate = dirInfo.LastWriteTimeUtc, - FileCount = dirInfo.GetFiles().Length - }); - } - - foreach (var filePath in Directory.GetFiles(targetPath)) - { - var fileInfo = new FileInfo(filePath); - - result.Files.Add(new ServiceModel.Types.File - { - Name = fileInfo.Name, - Extension = fileInfo.Extension, - FileSizeBytes = fileInfo.Length, - ModifiedDate = fileInfo.LastWriteTimeUtc, - IsTextFile = Config.TextFileExtensions.Contains(fileInfo.Extension), - }); - } - - return result; - } - - private FileInfo GetPath(Files request) - { - return new FileInfo(Path.Combine(this.Config.RootDirectory, request.Path.GetSafePath())); - } - - private FileInfo GetAndValidateExistingPath(Files request) - { - var targetFile = GetPath(request); - if (!targetFile.Exists && !Directory.Exists(targetFile.FullName)) - throw new HttpError(HttpStatusCode.NotFound, new FileNotFoundException("Could not find: " + request.Path)); - - return targetFile; - } - - private FileResult GetFileResult(FileInfo fileInfo) - { - var isTextFile = this.Config.TextFileExtensions.Contains(fileInfo.Extension); - - return new FileResult - { - Name = fileInfo.Name, - Extension = fileInfo.Extension, - FileSizeBytes = fileInfo.Length, - IsTextFile = isTextFile, - Contents = isTextFile ? File.ReadAllText(fileInfo.FullName) : null, - ModifiedDate = fileInfo.LastWriteTimeUtc, - }; - } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles.ServiceInterface/Properties/AssemblyInfo.cs b/src/RestFiles/RestFiles.ServiceInterface/Properties/AssemblyInfo.cs deleted file mode 100644 index 3417018..0000000 --- a/src/RestFiles/RestFiles.ServiceInterface/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("RemoteInfo.ServiceInterface")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Microsoft")] -[assembly: AssemblyProduct("RemoteInfo.ServiceInterface")] -[assembly: AssemblyCopyright("Copyright © Microsoft 2009")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("7d720cea-5beb-4a9a-9fa7-781ffd259551")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/RestFiles/RestFiles.ServiceInterface/RestFiles.ServiceInterface.csproj b/src/RestFiles/RestFiles.ServiceInterface/RestFiles.ServiceInterface.csproj deleted file mode 100644 index 5881351..0000000 --- a/src/RestFiles/RestFiles.ServiceInterface/RestFiles.ServiceInterface.csproj +++ /dev/null @@ -1,129 +0,0 @@ - - - - Debug - AnyCPU - 9.0.30729 - 2.0 - {5B91935A-7ED6-4496-871B-6AD25BC3F097} - Library - Properties - RestFiles.ServiceInterface - RestFiles.ServiceInterface - v4.5 - 512 - - - 3.5 - - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - false - true - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - AllRules.ruleset - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - AllRules.ruleset - false - - - - ..\packages\ServiceStack.4.5.0\lib\net45\ServiceStack.dll - True - - - ..\packages\ServiceStack.Client.4.5.0\lib\net45\ServiceStack.Client.dll - True - - - ..\packages\ServiceStack.Common.4.5.0\lib\net45\ServiceStack.Common.dll - True - - - ..\packages\ServiceStack.Interfaces.4.5.0\lib\portable-wp80+sl5+net45+win8+wpa81+monotouch+monoandroid+xamarin.ios10\ServiceStack.Interfaces.dll - True - - - ..\packages\ServiceStack.Text.4.5.0\lib\net45\ServiceStack.Text.dll - True - - - - 3.5 - - - 3.5 - - - - - - - - - - - - - - {6BBEDCA5-1183-49EE-AE80-0269DEB2EDEF} - RestFiles.ServiceModel - - - - - False - .NET Framework 3.5 SP1 Client Profile - false - - - False - .NET Framework 3.5 SP1 - true - - - False - Windows Installer 3.1 - true - - - - - - - - \ No newline at end of file diff --git a/src/RestFiles/RestFiles.ServiceInterface/RevertFilesService.cs b/src/RestFiles/RestFiles.ServiceInterface/RevertFilesService.cs deleted file mode 100644 index a463977..0000000 --- a/src/RestFiles/RestFiles.ServiceInterface/RevertFilesService.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.IO; -using System.Linq; -using RestFiles.ServiceModel; -using ServiceStack; - -namespace RestFiles.ServiceInterface -{ - /// - /// Define your ServiceStack web service request (i.e. Request DTO). - /// - public class RevertFilesService : Service - { - /// - /// Gets or sets the AppConfig. The built-in IoC used with ServiceStack autowires this property. - /// - public AppConfig Config { get; set; } - - public object Post(RevertFiles request) - { - var rootDir = Config.RootDirectory; - - if (Directory.Exists(rootDir)) - { - Directory.Delete(rootDir, true); - } - - CopyFiles(rootDir, "~/App_Data/src".MapHostAbsolutePath(), ".cs", ".htm", ".md"); - - var servicesDir = Path.Combine(rootDir, "services"); - CopyFiles(servicesDir, "~/App_Data/src/RestFiles.ServiceInterface/".MapHostAbsolutePath(), "Service.cs"); - - var testsDir = Path.Combine(rootDir, "tests"); - CopyFiles(testsDir, "~/App_Data/src/RestFiles.Tests/".MapHostAbsolutePath(), ".cs"); - - var dtosDir = Path.Combine(rootDir, "dtos"); - - var opsDtoPath = dtosDir; - CopyFiles(opsDtoPath, "~/App_Data/src/RestFiles.ServiceModel/".MapHostAbsolutePath()); - - var typesDtoPath = Path.Combine(dtosDir, "Types"); - CopyFiles(typesDtoPath, "~/App_Data/src/RestFiles.ServiceModel/Types/".MapHostAbsolutePath()); - - return new RevertFilesResponse(); - } - - private static void CopyFiles(string path, string filesPath, params string[] excludedFiles) - { - Directory.CreateDirectory(path); - var files = Directory.GetFiles(filesPath); - foreach (var file in files) - { - if (excludedFiles.IsEmpty() || excludedFiles.Any(x => file.EndsWith(x))) - { - var fileName = Path.GetFileName(file); - if (file.EndsWith(".cs")) - { - fileName += ".txt"; - } - File.Copy(file, Path.Combine(path, fileName)); - } - } - } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles.ServiceInterface/packages.config b/src/RestFiles/RestFiles.ServiceInterface/packages.config deleted file mode 100644 index 6b7d575..0000000 --- a/src/RestFiles/RestFiles.ServiceInterface/packages.config +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/RestFiles/RestFiles.ServiceModel/Properties/AssemblyInfo.cs b/src/RestFiles/RestFiles.ServiceModel/Properties/AssemblyInfo.cs deleted file mode 100644 index 6082597..0000000 --- a/src/RestFiles/RestFiles.ServiceModel/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Serialization; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("ServiceStack.RemoteInfo.ServiceModel")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Microsoft")] -[assembly: AssemblyProduct("ServiceStack.RemoteInfo.ServiceModel")] -[assembly: AssemblyCopyright("Copyright © Microsoft 2009")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("9d974c90-d810-4d93-af14-7fec4bbc9f50")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] - -[assembly: ContractNamespace("http://schemas.servicestack.net/types", ClrNamespace = "RestFiles.ServiceModel.Operations")] -[assembly: ContractNamespace("http://schemas.servicestack.net/types", ClrNamespace = "RestFiles.ServiceModel.Types")] diff --git a/src/RestFiles/RestFiles.ServiceModel/RestFiles.ServiceModel.csproj b/src/RestFiles/RestFiles.ServiceModel/RestFiles.ServiceModel.csproj deleted file mode 100644 index 606c63f..0000000 --- a/src/RestFiles/RestFiles.ServiceModel/RestFiles.ServiceModel.csproj +++ /dev/null @@ -1,113 +0,0 @@ - - - - Debug - AnyCPU - 9.0.30729 - 2.0 - {6BBEDCA5-1183-49EE-AE80-0269DEB2EDEF} - Library - Properties - RestFiles.ServiceModel - RestFiles.ServiceModel - v4.5 - 512 - - - 3.5 - - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - false - true - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - AllRules.ruleset - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - AllRules.ruleset - false - - - - ..\packages\ServiceStack.Interfaces.4.5.0\lib\portable-wp80+sl5+net45+win8+wpa81+monotouch+monoandroid+xamarin.ios10\ServiceStack.Interfaces.dll - True - - - - 3.5 - - - 3.5 - - - - - - 3.0 - - - - - - - - - - - - - - False - .NET Framework 3.5 SP1 Client Profile - false - - - False - .NET Framework 3.5 SP1 - true - - - False - Windows Installer 3.1 - true - - - - - - - - \ No newline at end of file diff --git a/src/RestFiles/RestFiles.ServiceModel/Types/File.cs b/src/RestFiles/RestFiles.ServiceModel/Types/File.cs deleted file mode 100644 index de717f8..0000000 --- a/src/RestFiles/RestFiles.ServiceModel/Types/File.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace RestFiles.ServiceModel.Types -{ - public class File - { - public string Name { get; set; } - public string Extension { get; set; } - public long FileSizeBytes { get; set; } - public DateTime ModifiedDate { get; set; } - public bool IsTextFile { get; set; } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles.ServiceModel/Types/FileResult.cs b/src/RestFiles/RestFiles.ServiceModel/Types/FileResult.cs deleted file mode 100644 index 13a3634..0000000 --- a/src/RestFiles/RestFiles.ServiceModel/Types/FileResult.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace RestFiles.ServiceModel.Types -{ - public class FileResult - { - public string Name { get; set; } - public string Extension { get; set; } - public long FileSizeBytes { get; set; } - public DateTime ModifiedDate { get; set; } - public bool IsTextFile { get; set; } - public string Contents { get; set; } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles.ServiceModel/Types/Folder.cs b/src/RestFiles/RestFiles.ServiceModel/Types/Folder.cs deleted file mode 100644 index 9594b3f..0000000 --- a/src/RestFiles/RestFiles.ServiceModel/Types/Folder.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace RestFiles.ServiceModel.Types -{ - public class Folder - { - public string Name { get; set; } - public DateTime ModifiedDate { get; set; } - public int FileCount { get; set; } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles.ServiceModel/Types/FolderResult.cs b/src/RestFiles/RestFiles.ServiceModel/Types/FolderResult.cs deleted file mode 100644 index 2a0f551..0000000 --- a/src/RestFiles/RestFiles.ServiceModel/Types/FolderResult.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; - -namespace RestFiles.ServiceModel.Types -{ - public class FolderResult - { - public FolderResult() - { - Folders = new List(); - Files = new List(); - } - - public List Folders { get; set; } - public List Files { get; set; } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles.ServiceModel/packages.config b/src/RestFiles/RestFiles.ServiceModel/packages.config deleted file mode 100644 index 5c87f86..0000000 --- a/src/RestFiles/RestFiles.ServiceModel/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/RestFiles/RestFiles.Tests/Properties/AssemblyInfo.cs b/src/RestFiles/RestFiles.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index fae6565..0000000 --- a/src/RestFiles/RestFiles.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("RemoteInfo.Tests.ConsoleClient")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Microsoft")] -[assembly: AssemblyProduct("RemoteInfo.Tests.ConsoleClient")] -[assembly: AssemblyCopyright("Copyright © Microsoft 2009")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("cb3a89fd-f212-410a-97de-d6f7d25f9f79")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/RestFiles/RestFiles.Tests/RestFiles.Tests.csproj b/src/RestFiles/RestFiles.Tests/RestFiles.Tests.csproj deleted file mode 100644 index 661bdfe..0000000 --- a/src/RestFiles/RestFiles.Tests/RestFiles.Tests.csproj +++ /dev/null @@ -1,136 +0,0 @@ - - - - Debug - AnyCPU - 9.0.21022 - 2.0 - {DAE4FFC2-B446-4020-8578-19B816FAA40A} - Library - Properties - RestFiles.Tests - RestFiles.Tests - v4.5 - 512 - - - - - 3.5 - - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - false - true - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - AllRules.ruleset - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - AllRules.ruleset - false - - - - ..\packages\NUnit.3.4.1\lib\net45\nunit.framework.dll - True - - - ..\packages\ServiceStack.4.5.0\lib\net45\ServiceStack.dll - True - - - ..\packages\ServiceStack.Client.4.5.0\lib\net45\ServiceStack.Client.dll - True - - - ..\packages\ServiceStack.Common.4.5.0\lib\net45\ServiceStack.Common.dll - True - - - ..\packages\ServiceStack.Interfaces.4.5.0\lib\portable-wp80+sl5+net45+win8+wpa81+monotouch+monoandroid+xamarin.ios10\ServiceStack.Interfaces.dll - True - - - ..\packages\ServiceStack.Text.4.5.0\lib\net45\ServiceStack.Text.dll - True - - - - 3.5 - - - - - - - - - - - {5B91935A-7ED6-4496-871B-6AD25BC3F097} - RestFiles.ServiceInterface - - - {6BBEDCA5-1183-49EE-AE80-0269DEB2EDEF} - RestFiles.ServiceModel - - - - - - - - False - .NET Framework 3.5 SP1 Client Profile - false - - - False - .NET Framework 3.5 SP1 - true - - - False - Windows Installer 3.1 - true - - - - - - - - \ No newline at end of file diff --git a/src/RestFiles/RestFiles.Tests/Support/RestFilesHttpListener.cs b/src/RestFiles/RestFiles.Tests/Support/RestFilesHttpListener.cs deleted file mode 100644 index 8050646..0000000 --- a/src/RestFiles/RestFiles.Tests/Support/RestFilesHttpListener.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Funq; -using RestFiles.ServiceInterface; -using ServiceStack; - -namespace RestFiles.Tests -{ - public class RestFilesHttpListener - : AppHostHttpListenerBase - { - public const string ListeningOn = "http://localhost:8080/"; - - public RestFilesHttpListener() - : base("HttpListener Hosts for Unit Tests", typeof(FilesService).Assembly) { } - - public AppConfig Config { get; set; } - - public override void Configure(Container container) - { - this.Config = new AppConfig - { - RootDirectory = "~/App_Data/files/".MapAbsolutePath(), - TextFileExtensions = ".txt,.sln,.proj,.cs,.config,.asax".Split(','), - }; - container.Register(this.Config); - - this.Start(ListeningOn); - } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles.Tests/SyncRestClientTests.cs b/src/RestFiles/RestFiles.Tests/SyncRestClientTests.cs deleted file mode 100644 index 1025228..0000000 --- a/src/RestFiles/RestFiles.Tests/SyncRestClientTests.cs +++ /dev/null @@ -1,227 +0,0 @@ -using System; -using System.IO; -using NUnit.Framework; -using RestFiles.ServiceModel; -using ServiceStack; - -/* For syntax highlighting and better readability of this file, view it on GitHub: - * https://github.com/ServiceStack/ServiceStack.Examples/blob/master/src/RestFiles/RestFiles.Tests/SyncRestClientTests.cs - */ - -namespace RestFiles.Tests -{ - /// - /// These test show how you can call ServiceStack REST web services synchronously using an IRestClient. - /// - /// Sync IO calls are the most familiar calling convention for .NET developers as it provides the simplest - /// API to use and requires the least code and effort as it allows you to program sequentially - /// - [TestFixture] - public class SyncRestClientTests - { - public const string WebServiceHostUrl = "http://localhost:8080/"; - private const string ReadmeFileContents = "THIS IS A README FILE"; - private const string ReplacedFileContents = "THIS README FILE HAS BEEN REPLACED"; - private const string TestUploadFileContents = "THIS FILE IS USED FOR UPLOADING IN TESTS"; - public string FilesRootDir; - - RestFilesHttpListener appHost; - - [TestFixtureSetUp] - public void TextFixtureSetUp() - { - appHost = new RestFilesHttpListener(); - appHost.Init(); - } - - [TestFixtureTearDown] - public void TestFixtureTearDown() - { - if (appHost != null) appHost.Dispose(); - appHost = null; - } - - [SetUp] - public void OnBeforeEachTest() - { - //Setup the files directory with some test files and folders - FilesRootDir = appHost.Config.RootDirectory; - if (Directory.Exists(FilesRootDir)) - { - Directory.Delete(FilesRootDir, true); - } - Directory.CreateDirectory(FilesRootDir + "SubFolder"); - Directory.CreateDirectory(FilesRootDir + "SubFolder2"); - File.WriteAllText(Path.Combine(FilesRootDir, "README.txt"), ReadmeFileContents); - File.WriteAllText(Path.Combine(FilesRootDir, "TESTUPLOAD.txt"), TestUploadFileContents); - } - - /// - /// Choose your favourite format to run tests with - /// - public IRestClient CreateRestClient() - { - return new JsonServiceClient(WebServiceHostUrl); //Best choice for Ajax web apps, 3x faster than XML - //return new XmlServiceClient(WebServiceHostUrl); //Ubiquitous structured data format best for supporting non .NET clients - //return new JsvServiceClient(WebServiceHostUrl); //Fastest, most compact and resilient format great for .NET to .NET client > server - } - - [Test] - public void Can_Get_to_retrieve_existing_file() - { - var restClient = CreateRestClient(); - - var response = restClient.Get("files/README.txt"); - - Assert.That(response.File.Contents, Is.EqualTo("THIS IS A README FILE")); - } - - [Test] - public void Can_Get_to_retrieve_existing_folder_listing() - { - var restClient = CreateRestClient(); - - var response = restClient.Get("files/"); - - Assert.That(response.Directory.Folders.Count, Is.EqualTo(2)); - Assert.That(response.Directory.Files.Count, Is.EqualTo(2)); - } - - [Test] - public void Can_Post_to_path_without_uploaded_files_to_create_a_new_Directory() - { - var restClient = CreateRestClient(); - - var response = restClient.Post("files/SubFolder/NewFolder", new Files()); - - Assert.That(Directory.Exists(FilesRootDir + "SubFolder/NewFolder")); - } - - [Test] - public void Can_WebRequest_POST_upload_file_to_save_new_file_and_create_new_Directory() - { - var restClient = CreateRestClient(); - - var fileToUpload = new FileInfo(FilesRootDir + "TESTUPLOAD.txt"); - - var response = restClient.PostFile("files/UploadedFiles/", - fileToUpload, MimeTypes.GetMimeType(fileToUpload.Name)); - - Assert.That(Directory.Exists(FilesRootDir + "UploadedFiles")); - Assert.That(File.ReadAllText(FilesRootDir + "UploadedFiles/TESTUPLOAD.txt"), - Is.EqualTo(TestUploadFileContents)); - } - - [Test] - public void Can_Put_to_replace_text_content_of_an_existing_file() - { - var restClient = CreateRestClient(); - - var response = restClient.Put(WebServiceHostUrl + "files/README.txt", - new Files { TextContents = ReplacedFileContents }); - - Assert.That(File.ReadAllText(FilesRootDir + "README.txt"), - Is.EqualTo(ReplacedFileContents)); - } - - [Test] - public void Can_Delete_to_replace_text_content_of_an_existing_file() - { - var restClient = CreateRestClient(); - - var response = restClient.Delete("files/README.txt"); - - Assert.That(!File.Exists(FilesRootDir + "README.txt")); - } - - - /* - * Error Handling Tests - */ - [Test] - public void GET_a_file_that_doesnt_exist_throws_a_404_FileNotFoundException() - { - var restClient = CreateRestClient(); - - try - { - var response = restClient.Get(WebServiceHostUrl + "files/UnknownFolder"); - - Assert.Fail("Should fail with 404 FileNotFoundException"); - } - catch (WebServiceException webEx) - { - Assert.That(webEx.StatusCode, Is.EqualTo(404)); - var response = (FilesResponse)webEx.ResponseDto; - Assert.That(response.ResponseStatus.ErrorCode, Is.EqualTo(typeof(FileNotFoundException).Name)); - Assert.That(response.ResponseStatus.Message, Is.EqualTo("Could not find: UnknownFolder")); - } - } - - [Test] - public void POST_to_an_existing_file_throws_a_500_NotSupportedException() - { - var restClient = CreateRestClient(); - - var fileToUpload = new FileInfo(FilesRootDir + "TESTUPLOAD.txt"); - - try - { - var response = restClient.PostFile(WebServiceHostUrl + "files/README.txt", - fileToUpload, MimeTypes.GetMimeType(fileToUpload.Name)); - - Assert.Fail("Should fail with NotSupportedException"); - } - catch (WebServiceException webEx) - { - Assert.That(webEx.StatusCode, Is.EqualTo(405)); - var response = (FilesResponse)webEx.ResponseDto; - Assert.That(response.ResponseStatus.ErrorCode, Is.EqualTo(typeof(NotSupportedException).Name)); - Assert.That(response.ResponseStatus.Message, - Is.EqualTo("POST only supports uploading new files. Use PUT to replace contents of an existing file")); - } - } - - [Test] - public void PUT_to_replace_a_non_existing_file_throws_404() - { - var restClient = CreateRestClient(); - - try - { - var response = restClient.Put(WebServiceHostUrl + "files/non-existing-file.txt", - new Files { TextContents = ReplacedFileContents }); - - Assert.Fail("Should fail with 404 FileNotFoundException"); - } - catch (WebServiceException webEx) - { - Assert.That(webEx.StatusCode, Is.EqualTo(404)); - var response = (FilesResponse)webEx.ResponseDto; - Assert.That(response.ResponseStatus.ErrorCode, Is.EqualTo(typeof(FileNotFoundException).Name)); - Assert.That(response.ResponseStatus.Message, Is.EqualTo("Could not find: non-existing-file.txt")); - } - } - - [Test] - public void DELETE_a_non_existing_file_throws_404() - { - var restClient = CreateRestClient(); - - try - { - var response = restClient.Delete(WebServiceHostUrl + "files/non-existing-file.txt"); - - Assert.Fail("Should fail with 404 FileNotFoundException"); - } - catch (WebServiceException webEx) - { - Assert.That(webEx.StatusCode, Is.EqualTo(404)); - var response = (FilesResponse)webEx.ResponseDto; - Assert.That(response.ResponseStatus.ErrorCode, Is.EqualTo(typeof(FileNotFoundException).Name)); - Assert.That(response.ResponseStatus.Message, Is.EqualTo("Could not find: non-existing-file.txt")); - } - } - - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles.Tests/packages.config b/src/RestFiles/RestFiles.Tests/packages.config deleted file mode 100644 index 7da91bc..0000000 --- a/src/RestFiles/RestFiles.Tests/packages.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/src/RestFiles/RestFiles.csproj b/src/RestFiles/RestFiles.csproj new file mode 100644 index 0000000..cb30ad5 --- /dev/null +++ b/src/RestFiles/RestFiles.csproj @@ -0,0 +1,18 @@ + + + + net5 + RestFiles + RestFiles + + + + + + + + + + + + diff --git a/src/RestFiles/RestFiles.sln b/src/RestFiles/RestFiles.sln deleted file mode 100644 index 89e4765..0000000 --- a/src/RestFiles/RestFiles.sln +++ /dev/null @@ -1,41 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 11.00 -# Visual Studio 2010 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestFiles", "RestFiles\RestFiles.csproj", "{70B9EEDE-BC2A-42EB-933D-A94D7D4275CB}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestFiles.ServiceInterface", "RestFiles.ServiceInterface\RestFiles.ServiceInterface.csproj", "{5B91935A-7ED6-4496-871B-6AD25BC3F097}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestFiles.ServiceModel", "RestFiles.ServiceModel\RestFiles.ServiceModel.csproj", "{6BBEDCA5-1183-49EE-AE80-0269DEB2EDEF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestFiles.Tests", "RestFiles.Tests\RestFiles.Tests.csproj", "{DAE4FFC2-B446-4020-8578-19B816FAA40A}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {70B9EEDE-BC2A-42EB-933D-A94D7D4275CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {70B9EEDE-BC2A-42EB-933D-A94D7D4275CB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {70B9EEDE-BC2A-42EB-933D-A94D7D4275CB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {70B9EEDE-BC2A-42EB-933D-A94D7D4275CB}.Release|Any CPU.Build.0 = Release|Any CPU - {5B91935A-7ED6-4496-871B-6AD25BC3F097}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5B91935A-7ED6-4496-871B-6AD25BC3F097}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5B91935A-7ED6-4496-871B-6AD25BC3F097}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5B91935A-7ED6-4496-871B-6AD25BC3F097}.Release|Any CPU.Build.0 = Release|Any CPU - {6BBEDCA5-1183-49EE-AE80-0269DEB2EDEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6BBEDCA5-1183-49EE-AE80-0269DEB2EDEF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6BBEDCA5-1183-49EE-AE80-0269DEB2EDEF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6BBEDCA5-1183-49EE-AE80-0269DEB2EDEF}.Release|Any CPU.Build.0 = Release|Any CPU - {DAE4FFC2-B446-4020-8578-19B816FAA40A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DAE4FFC2-B446-4020-8578-19B816FAA40A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DAE4FFC2-B446-4020-8578-19B816FAA40A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DAE4FFC2-B446-4020-8578-19B816FAA40A}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(MonoDevelopProperties) = preSolution - StartupItem = ..\Client\RemoteInfoClient\RemoteInfoClient.csproj - EndGlobalSection -EndGlobal diff --git a/src/RestFiles/RestFiles/App_Data/files/Global.asax.cs.txt b/src/RestFiles/RestFiles/App_Data/files/Global.asax.cs.txt deleted file mode 100644 index 937bf36..0000000 --- a/src/RestFiles/RestFiles/App_Data/files/Global.asax.cs.txt +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.IO; -using Funq; -using RestFiles.ServiceInterface; -using ServiceStack; -using ServiceStack.Configuration; - -namespace RestFiles -{ - /// - /// Create your ServiceStack web service application with a singleton AppHost. - /// - public class AppHost : AppHostBase - { - /// - /// Initializes a new instance of your ServiceStack application, with the specified name and assembly containing the services. - /// - public AppHost() : base("REST Files", typeof(FilesService).Assembly) {} - - /// - /// Configure the container with the necessary routes for your ServiceStack application. - /// - /// The built-in IoC used with ServiceStack. - public override void Configure(Container container) - { - //Permit modern browsers (e.g. Firefox) to allow sending of any REST HTTP Method - Plugins.Add(new CorsFeature()); - - SetConfig(new HostConfig { - DebugMode = true, - }); - - var config = new AppConfig(new AppSettings()); - container.Register(config); - - if (!Directory.Exists(config.RootDirectory)) - { - Directory.CreateDirectory(config.RootDirectory); - } - } - } - - public class Global : System.Web.HttpApplication - { - protected void Application_Start(object sender, EventArgs e) - { - //Initialize your application - (new AppHost()).Init(); - } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/files/default.htm b/src/RestFiles/RestFiles/App_Data/files/default.htm deleted file mode 100644 index 48d01fb..0000000 --- a/src/RestFiles/RestFiles/App_Data/files/default.htm +++ /dev/null @@ -1,350 +0,0 @@ - - - - REST Files: GitHub-like file access with 1 page jQuery + 1 C# REST Service - - - - - - - - ServiceStack Home - - -
-
- .NET Clients can access the File Service API with: - Async C# client examples Sync - C# client examples -
-

- GitHub-like browser with complete remote file management over REST in - 1 page jQuery + - 1 page C# -

-
-

- The REST /files web service API:

-
-
GET /path/to/folder
-
- Directory listing at server
-
GET /path/to/file.txt
-
- Info and contents of text file at path
-
POST /path/to/newfolder (no-body)
-
- Create a directory at newfolder path
-
POST /path/to/folder (multipart/form-data)
-
- Upload file at folder path
-
PUT /path/to/file.txt
-
- Replace contents of text file at path
-
DELETE /path/to/folder
-
- Delete folder at path
-
-
-
- revert files - -
-
- -
-
- -
-
- -

- Stop! old, outdated browser detected.

-

- Unfortunately the advanced techniques used on this page is not supported by your - current browser. -

-

- To have a better browser experience for this and other web sites, please choose - from one of the better alternatives below: -

- - Chrome - Opera - Firefox - Safari -
-
- - - - - diff --git a/src/RestFiles/RestFiles/App_Data/files/dtos/Files.cs.txt b/src/RestFiles/RestFiles/App_Data/files/dtos/Files.cs.txt deleted file mode 100644 index 30a02f8..0000000 --- a/src/RestFiles/RestFiles/App_Data/files/dtos/Files.cs.txt +++ /dev/null @@ -1,34 +0,0 @@ -using RestFiles.ServiceModel.Types; -using ServiceStack; - -namespace RestFiles.ServiceModel -{ - /// - /// Define your ServiceStack web service request (i.e. the Request DTO). - /// - [Api("GET the File or Directory info at {Path}\n" - + "POST multipart/formdata to upload a new file to any {Path} in the /ReadWrite folder\n" - + "PUT {TextContents} to replace the contents of a text file in the /ReadWrite folder\n")] - [Route("/files")] - [Route("/files/{Path*}")] - public class Files - { - public string Path { get; set; } - public string TextContents { get; set; } - public bool ForDownload { get; set; } - } - - /// - /// Define your ServiceStack web service response (i.e. Response DTO). - /// - public class FilesResponse : IHasResponseStatus - { - public FolderResult Directory { get; set; } - public FileResult File { get; set; } - - /// - /// Gets or sets the ResponseStatus. The built-in IoC used with ServiceStack autowires this property. - /// - public ResponseStatus ResponseStatus { get; set; } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/files/dtos/RevertFiles.cs.txt b/src/RestFiles/RestFiles/App_Data/files/dtos/RevertFiles.cs.txt deleted file mode 100644 index d9ccb44..0000000 --- a/src/RestFiles/RestFiles/App_Data/files/dtos/RevertFiles.cs.txt +++ /dev/null @@ -1,27 +0,0 @@ -using ServiceStack; - -namespace RestFiles.ServiceModel -{ - /// - /// Define your ServiceStack web service request (i.e. the Request DTO). - /// - [Route("/revertfiles")] - public class RevertFiles { } - - /// - /// Define your ServiceStack web service response (i.e. Response DTO). - /// - public class RevertFilesResponse : IHasResponseStatus - { - public RevertFilesResponse() - { - //Comment this out if you wish to receive the response status. - this.ResponseStatus = new ResponseStatus(); - } - - /// - /// Gets or sets the ResponseStatus. The built-in Ioc used with ServiceStack autowires this property with service exceptions. - /// - public ResponseStatus ResponseStatus { get; set; } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/files/dtos/Types/File.cs.txt b/src/RestFiles/RestFiles/App_Data/files/dtos/Types/File.cs.txt deleted file mode 100644 index de717f8..0000000 --- a/src/RestFiles/RestFiles/App_Data/files/dtos/Types/File.cs.txt +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace RestFiles.ServiceModel.Types -{ - public class File - { - public string Name { get; set; } - public string Extension { get; set; } - public long FileSizeBytes { get; set; } - public DateTime ModifiedDate { get; set; } - public bool IsTextFile { get; set; } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/files/dtos/Types/FileResult.cs.txt b/src/RestFiles/RestFiles/App_Data/files/dtos/Types/FileResult.cs.txt deleted file mode 100644 index 13a3634..0000000 --- a/src/RestFiles/RestFiles/App_Data/files/dtos/Types/FileResult.cs.txt +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace RestFiles.ServiceModel.Types -{ - public class FileResult - { - public string Name { get; set; } - public string Extension { get; set; } - public long FileSizeBytes { get; set; } - public DateTime ModifiedDate { get; set; } - public bool IsTextFile { get; set; } - public string Contents { get; set; } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/files/dtos/Types/Folder.cs.txt b/src/RestFiles/RestFiles/App_Data/files/dtos/Types/Folder.cs.txt deleted file mode 100644 index 9594b3f..0000000 --- a/src/RestFiles/RestFiles/App_Data/files/dtos/Types/Folder.cs.txt +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace RestFiles.ServiceModel.Types -{ - public class Folder - { - public string Name { get; set; } - public DateTime ModifiedDate { get; set; } - public int FileCount { get; set; } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/files/dtos/Types/FolderResult.cs.txt b/src/RestFiles/RestFiles/App_Data/files/dtos/Types/FolderResult.cs.txt deleted file mode 100644 index 2a0f551..0000000 --- a/src/RestFiles/RestFiles/App_Data/files/dtos/Types/FolderResult.cs.txt +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; - -namespace RestFiles.ServiceModel.Types -{ - public class FolderResult - { - public FolderResult() - { - Folders = new List(); - Files = new List(); - } - - public List Folders { get; set; } - public List Files { get; set; } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/files/services/FilesService.cs.txt b/src/RestFiles/RestFiles/App_Data/files/services/FilesService.cs.txt deleted file mode 100644 index 887d521..0000000 --- a/src/RestFiles/RestFiles/App_Data/files/services/FilesService.cs.txt +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.IO; -using System.Net; -using RestFiles.ServiceInterface.Support; -using RestFiles.ServiceModel; -using RestFiles.ServiceModel.Types; -using ServiceStack; -using File = System.IO.File; - -namespace RestFiles.ServiceInterface -{ - /// - /// Define your ServiceStack web service request (i.e. Request DTO). - /// - public class FilesService : Service - { - /// - /// Gets or sets the AppConfig. The built-in IoC used with ServiceStack autowires this property. - /// - public AppConfig Config { get; set; } - - public object Get(Files request) - { - var targetFile = GetAndValidateExistingPath(request); - - var isDirectory = Directory.Exists(targetFile.FullName); - - if (!isDirectory && request.ForDownload) - return new HttpResult(targetFile, asAttachment: true); - - var response = isDirectory - ? new FilesResponse { Directory = GetFolderResult(targetFile.FullName) } - : new FilesResponse { File = GetFileResult(targetFile) }; - - return response; - } - - public void Post(Files request) - { - var targetDir = GetPath(request); - - var isExistingFile = targetDir.Exists - && (targetDir.Attributes & FileAttributes.Directory) != FileAttributes.Directory; - - if (isExistingFile) - throw new NotSupportedException( - "POST only supports uploading new files. Use PUT to replace contents of an existing file"); - - if (!Directory.Exists(targetDir.FullName)) - Directory.CreateDirectory(targetDir.FullName); - - foreach (var uploadedFile in base.Request.Files) - { - var newFilePath = Path.Combine(targetDir.FullName, uploadedFile.FileName); - uploadedFile.SaveTo(newFilePath); - } - } - - public void Put(Files request) - { - var targetFile = GetAndValidateExistingPath(request); - - if (!this.Config.TextFileExtensions.Contains(targetFile.Extension)) - throw new NotSupportedException("PUT Can only update text files, not: " + targetFile.Extension); - - if (request.TextContents == null) - throw new ArgumentNullException("TextContents"); - - File.WriteAllText(targetFile.FullName, request.TextContents); - } - - public void Delete(Files request) - { - var targetFile = GetAndValidateExistingPath(request); - File.Delete(targetFile.FullName); - } - - private FolderResult GetFolderResult(string targetPath) - { - var result = new FolderResult(); - - foreach (var dirPath in Directory.GetDirectories(targetPath)) - { - var dirInfo = new DirectoryInfo(dirPath); - - if (this.Config.ExcludeDirectories.Contains(dirInfo.Name)) continue; - - result.Folders.Add(new Folder - { - Name = dirInfo.Name, - ModifiedDate = dirInfo.LastWriteTimeUtc, - FileCount = dirInfo.GetFiles().Length - }); - } - - foreach (var filePath in Directory.GetFiles(targetPath)) - { - var fileInfo = new FileInfo(filePath); - - result.Files.Add(new ServiceModel.Types.File - { - Name = fileInfo.Name, - Extension = fileInfo.Extension, - FileSizeBytes = fileInfo.Length, - ModifiedDate = fileInfo.LastWriteTimeUtc, - IsTextFile = Config.TextFileExtensions.Contains(fileInfo.Extension), - }); - } - - return result; - } - - private FileInfo GetPath(Files request) - { - return new FileInfo(Path.Combine(this.Config.RootDirectory, request.Path.GetSafePath())); - } - - private FileInfo GetAndValidateExistingPath(Files request) - { - var targetFile = GetPath(request); - if (!targetFile.Exists && !Directory.Exists(targetFile.FullName)) - throw new HttpError(HttpStatusCode.NotFound, new FileNotFoundException("Could not find: " + request.Path)); - - return targetFile; - } - - private FileResult GetFileResult(FileInfo fileInfo) - { - var isTextFile = this.Config.TextFileExtensions.Contains(fileInfo.Extension); - - return new FileResult - { - Name = fileInfo.Name, - Extension = fileInfo.Extension, - FileSizeBytes = fileInfo.Length, - IsTextFile = isTextFile, - Contents = isTextFile ? File.ReadAllText(fileInfo.FullName) : null, - ModifiedDate = fileInfo.LastWriteTimeUtc, - }; - } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/files/services/RevertFilesService.cs.txt b/src/RestFiles/RestFiles/App_Data/files/services/RevertFilesService.cs.txt deleted file mode 100644 index b911640..0000000 --- a/src/RestFiles/RestFiles/App_Data/files/services/RevertFilesService.cs.txt +++ /dev/null @@ -1,64 +0,0 @@ -using System.IO; -using System.Linq; -using RestFiles.ServiceModel; -using ServiceStack; - -namespace RestFiles.ServiceInterface -{ - /// - /// Define your ServiceStack web service request (i.e. Request DTO). - /// - public class RevertFilesService : Service - { - /// - /// Gets or sets the AppConfig. The built-in IoC used with ServiceStack autowires this property. - /// - public AppConfig Config { get; set; } - - public object Post(RevertFiles request) - { - var rootDir = Config.RootDirectory; - - if (Directory.Exists(rootDir)) - { - Directory.Delete(rootDir, true); - } - - CopyFiles(rootDir, "~/".MapHostAbsolutePath(), ".cs", ".htm", ".md"); - - var servicesDir = Path.Combine(rootDir, "services"); - CopyFiles(servicesDir, "~/../RestFiles.ServiceInterface/".MapHostAbsolutePath(), "Service.cs"); - - var testsDir = Path.Combine(rootDir, "tests"); - CopyFiles(testsDir, "~/../RestFiles.Tests/".MapHostAbsolutePath(), ".cs"); - - var dtosDir = Path.Combine(rootDir, "dtos"); - - var opsDtoPath = dtosDir; - CopyFiles(opsDtoPath, "~/../RestFiles.ServiceModel/".MapHostAbsolutePath()); - - var typesDtoPath = Path.Combine(dtosDir, "Types"); - CopyFiles(typesDtoPath, "~/../RestFiles.ServiceModel/Types/".MapHostAbsolutePath()); - - return new RevertFilesResponse(); - } - - private static void CopyFiles(string path, string filesPath, params string[] excludedFiles) - { - Directory.CreateDirectory(path); - var files = Directory.GetFiles(filesPath); - foreach (var file in files) - { - if (excludedFiles.IsEmpty() || excludedFiles.Any(x => file.EndsWith(x))) - { - var fileName = Path.GetFileName(file); - if (file.EndsWith(".cs")) - { - fileName += ".txt"; - } - File.Copy(file, Path.Combine(path, fileName)); - } - } - } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/files/tests/SyncRestClientTests.cs.txt b/src/RestFiles/RestFiles/App_Data/files/tests/SyncRestClientTests.cs.txt deleted file mode 100644 index 1025228..0000000 --- a/src/RestFiles/RestFiles/App_Data/files/tests/SyncRestClientTests.cs.txt +++ /dev/null @@ -1,227 +0,0 @@ -using System; -using System.IO; -using NUnit.Framework; -using RestFiles.ServiceModel; -using ServiceStack; - -/* For syntax highlighting and better readability of this file, view it on GitHub: - * https://github.com/ServiceStack/ServiceStack.Examples/blob/master/src/RestFiles/RestFiles.Tests/SyncRestClientTests.cs - */ - -namespace RestFiles.Tests -{ - /// - /// These test show how you can call ServiceStack REST web services synchronously using an IRestClient. - /// - /// Sync IO calls are the most familiar calling convention for .NET developers as it provides the simplest - /// API to use and requires the least code and effort as it allows you to program sequentially - /// - [TestFixture] - public class SyncRestClientTests - { - public const string WebServiceHostUrl = "http://localhost:8080/"; - private const string ReadmeFileContents = "THIS IS A README FILE"; - private const string ReplacedFileContents = "THIS README FILE HAS BEEN REPLACED"; - private const string TestUploadFileContents = "THIS FILE IS USED FOR UPLOADING IN TESTS"; - public string FilesRootDir; - - RestFilesHttpListener appHost; - - [TestFixtureSetUp] - public void TextFixtureSetUp() - { - appHost = new RestFilesHttpListener(); - appHost.Init(); - } - - [TestFixtureTearDown] - public void TestFixtureTearDown() - { - if (appHost != null) appHost.Dispose(); - appHost = null; - } - - [SetUp] - public void OnBeforeEachTest() - { - //Setup the files directory with some test files and folders - FilesRootDir = appHost.Config.RootDirectory; - if (Directory.Exists(FilesRootDir)) - { - Directory.Delete(FilesRootDir, true); - } - Directory.CreateDirectory(FilesRootDir + "SubFolder"); - Directory.CreateDirectory(FilesRootDir + "SubFolder2"); - File.WriteAllText(Path.Combine(FilesRootDir, "README.txt"), ReadmeFileContents); - File.WriteAllText(Path.Combine(FilesRootDir, "TESTUPLOAD.txt"), TestUploadFileContents); - } - - /// - /// Choose your favourite format to run tests with - /// - public IRestClient CreateRestClient() - { - return new JsonServiceClient(WebServiceHostUrl); //Best choice for Ajax web apps, 3x faster than XML - //return new XmlServiceClient(WebServiceHostUrl); //Ubiquitous structured data format best for supporting non .NET clients - //return new JsvServiceClient(WebServiceHostUrl); //Fastest, most compact and resilient format great for .NET to .NET client > server - } - - [Test] - public void Can_Get_to_retrieve_existing_file() - { - var restClient = CreateRestClient(); - - var response = restClient.Get("files/README.txt"); - - Assert.That(response.File.Contents, Is.EqualTo("THIS IS A README FILE")); - } - - [Test] - public void Can_Get_to_retrieve_existing_folder_listing() - { - var restClient = CreateRestClient(); - - var response = restClient.Get("files/"); - - Assert.That(response.Directory.Folders.Count, Is.EqualTo(2)); - Assert.That(response.Directory.Files.Count, Is.EqualTo(2)); - } - - [Test] - public void Can_Post_to_path_without_uploaded_files_to_create_a_new_Directory() - { - var restClient = CreateRestClient(); - - var response = restClient.Post("files/SubFolder/NewFolder", new Files()); - - Assert.That(Directory.Exists(FilesRootDir + "SubFolder/NewFolder")); - } - - [Test] - public void Can_WebRequest_POST_upload_file_to_save_new_file_and_create_new_Directory() - { - var restClient = CreateRestClient(); - - var fileToUpload = new FileInfo(FilesRootDir + "TESTUPLOAD.txt"); - - var response = restClient.PostFile("files/UploadedFiles/", - fileToUpload, MimeTypes.GetMimeType(fileToUpload.Name)); - - Assert.That(Directory.Exists(FilesRootDir + "UploadedFiles")); - Assert.That(File.ReadAllText(FilesRootDir + "UploadedFiles/TESTUPLOAD.txt"), - Is.EqualTo(TestUploadFileContents)); - } - - [Test] - public void Can_Put_to_replace_text_content_of_an_existing_file() - { - var restClient = CreateRestClient(); - - var response = restClient.Put(WebServiceHostUrl + "files/README.txt", - new Files { TextContents = ReplacedFileContents }); - - Assert.That(File.ReadAllText(FilesRootDir + "README.txt"), - Is.EqualTo(ReplacedFileContents)); - } - - [Test] - public void Can_Delete_to_replace_text_content_of_an_existing_file() - { - var restClient = CreateRestClient(); - - var response = restClient.Delete("files/README.txt"); - - Assert.That(!File.Exists(FilesRootDir + "README.txt")); - } - - - /* - * Error Handling Tests - */ - [Test] - public void GET_a_file_that_doesnt_exist_throws_a_404_FileNotFoundException() - { - var restClient = CreateRestClient(); - - try - { - var response = restClient.Get(WebServiceHostUrl + "files/UnknownFolder"); - - Assert.Fail("Should fail with 404 FileNotFoundException"); - } - catch (WebServiceException webEx) - { - Assert.That(webEx.StatusCode, Is.EqualTo(404)); - var response = (FilesResponse)webEx.ResponseDto; - Assert.That(response.ResponseStatus.ErrorCode, Is.EqualTo(typeof(FileNotFoundException).Name)); - Assert.That(response.ResponseStatus.Message, Is.EqualTo("Could not find: UnknownFolder")); - } - } - - [Test] - public void POST_to_an_existing_file_throws_a_500_NotSupportedException() - { - var restClient = CreateRestClient(); - - var fileToUpload = new FileInfo(FilesRootDir + "TESTUPLOAD.txt"); - - try - { - var response = restClient.PostFile(WebServiceHostUrl + "files/README.txt", - fileToUpload, MimeTypes.GetMimeType(fileToUpload.Name)); - - Assert.Fail("Should fail with NotSupportedException"); - } - catch (WebServiceException webEx) - { - Assert.That(webEx.StatusCode, Is.EqualTo(405)); - var response = (FilesResponse)webEx.ResponseDto; - Assert.That(response.ResponseStatus.ErrorCode, Is.EqualTo(typeof(NotSupportedException).Name)); - Assert.That(response.ResponseStatus.Message, - Is.EqualTo("POST only supports uploading new files. Use PUT to replace contents of an existing file")); - } - } - - [Test] - public void PUT_to_replace_a_non_existing_file_throws_404() - { - var restClient = CreateRestClient(); - - try - { - var response = restClient.Put(WebServiceHostUrl + "files/non-existing-file.txt", - new Files { TextContents = ReplacedFileContents }); - - Assert.Fail("Should fail with 404 FileNotFoundException"); - } - catch (WebServiceException webEx) - { - Assert.That(webEx.StatusCode, Is.EqualTo(404)); - var response = (FilesResponse)webEx.ResponseDto; - Assert.That(response.ResponseStatus.ErrorCode, Is.EqualTo(typeof(FileNotFoundException).Name)); - Assert.That(response.ResponseStatus.Message, Is.EqualTo("Could not find: non-existing-file.txt")); - } - } - - [Test] - public void DELETE_a_non_existing_file_throws_404() - { - var restClient = CreateRestClient(); - - try - { - var response = restClient.Delete(WebServiceHostUrl + "files/non-existing-file.txt"); - - Assert.Fail("Should fail with 404 FileNotFoundException"); - } - catch (WebServiceException webEx) - { - Assert.That(webEx.StatusCode, Is.EqualTo(404)); - var response = (FilesResponse)webEx.ResponseDto; - Assert.That(response.ResponseStatus.ErrorCode, Is.EqualTo(typeof(FileNotFoundException).Name)); - Assert.That(response.ResponseStatus.Message, Is.EqualTo("Could not find: non-existing-file.txt")); - } - } - - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/src/Global.asax.cs b/src/RestFiles/RestFiles/App_Data/src/Global.asax.cs deleted file mode 100644 index 937bf36..0000000 --- a/src/RestFiles/RestFiles/App_Data/src/Global.asax.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.IO; -using Funq; -using RestFiles.ServiceInterface; -using ServiceStack; -using ServiceStack.Configuration; - -namespace RestFiles -{ - /// - /// Create your ServiceStack web service application with a singleton AppHost. - /// - public class AppHost : AppHostBase - { - /// - /// Initializes a new instance of your ServiceStack application, with the specified name and assembly containing the services. - /// - public AppHost() : base("REST Files", typeof(FilesService).Assembly) {} - - /// - /// Configure the container with the necessary routes for your ServiceStack application. - /// - /// The built-in IoC used with ServiceStack. - public override void Configure(Container container) - { - //Permit modern browsers (e.g. Firefox) to allow sending of any REST HTTP Method - Plugins.Add(new CorsFeature()); - - SetConfig(new HostConfig { - DebugMode = true, - }); - - var config = new AppConfig(new AppSettings()); - container.Register(config); - - if (!Directory.Exists(config.RootDirectory)) - { - Directory.CreateDirectory(config.RootDirectory); - } - } - } - - public class Global : System.Web.HttpApplication - { - protected void Application_Start(object sender, EventArgs e) - { - //Initialize your application - (new AppHost()).Init(); - } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceInterface/AppConfig.cs b/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceInterface/AppConfig.cs deleted file mode 100644 index d97082d..0000000 --- a/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceInterface/AppConfig.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using ServiceStack; -using ServiceStack.Configuration; - -namespace RestFiles.ServiceInterface -{ - public class AppConfig - { - public AppConfig() - { - this.TextFileExtensions = new List(); - this.ExcludeDirectories = new List(); - } - - public AppConfig(IAppSettings resources) - { - this.RootDirectory = resources.GetString("RootDirectory").MapHostAbsolutePath() - .Replace('\\', Path.DirectorySeparatorChar); - - this.TextFileExtensions = resources.GetList("TextFileExtensions"); - this.ExcludeDirectories = resources.GetList("ExcludeDirectories"); - } - - public string RootDirectory { get; set; } - - public IList TextFileExtensions { get; set; } - - public IList ExcludeDirectories { get; set; } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceInterface/FilesService.cs b/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceInterface/FilesService.cs deleted file mode 100644 index 887d521..0000000 --- a/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceInterface/FilesService.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.IO; -using System.Net; -using RestFiles.ServiceInterface.Support; -using RestFiles.ServiceModel; -using RestFiles.ServiceModel.Types; -using ServiceStack; -using File = System.IO.File; - -namespace RestFiles.ServiceInterface -{ - /// - /// Define your ServiceStack web service request (i.e. Request DTO). - /// - public class FilesService : Service - { - /// - /// Gets or sets the AppConfig. The built-in IoC used with ServiceStack autowires this property. - /// - public AppConfig Config { get; set; } - - public object Get(Files request) - { - var targetFile = GetAndValidateExistingPath(request); - - var isDirectory = Directory.Exists(targetFile.FullName); - - if (!isDirectory && request.ForDownload) - return new HttpResult(targetFile, asAttachment: true); - - var response = isDirectory - ? new FilesResponse { Directory = GetFolderResult(targetFile.FullName) } - : new FilesResponse { File = GetFileResult(targetFile) }; - - return response; - } - - public void Post(Files request) - { - var targetDir = GetPath(request); - - var isExistingFile = targetDir.Exists - && (targetDir.Attributes & FileAttributes.Directory) != FileAttributes.Directory; - - if (isExistingFile) - throw new NotSupportedException( - "POST only supports uploading new files. Use PUT to replace contents of an existing file"); - - if (!Directory.Exists(targetDir.FullName)) - Directory.CreateDirectory(targetDir.FullName); - - foreach (var uploadedFile in base.Request.Files) - { - var newFilePath = Path.Combine(targetDir.FullName, uploadedFile.FileName); - uploadedFile.SaveTo(newFilePath); - } - } - - public void Put(Files request) - { - var targetFile = GetAndValidateExistingPath(request); - - if (!this.Config.TextFileExtensions.Contains(targetFile.Extension)) - throw new NotSupportedException("PUT Can only update text files, not: " + targetFile.Extension); - - if (request.TextContents == null) - throw new ArgumentNullException("TextContents"); - - File.WriteAllText(targetFile.FullName, request.TextContents); - } - - public void Delete(Files request) - { - var targetFile = GetAndValidateExistingPath(request); - File.Delete(targetFile.FullName); - } - - private FolderResult GetFolderResult(string targetPath) - { - var result = new FolderResult(); - - foreach (var dirPath in Directory.GetDirectories(targetPath)) - { - var dirInfo = new DirectoryInfo(dirPath); - - if (this.Config.ExcludeDirectories.Contains(dirInfo.Name)) continue; - - result.Folders.Add(new Folder - { - Name = dirInfo.Name, - ModifiedDate = dirInfo.LastWriteTimeUtc, - FileCount = dirInfo.GetFiles().Length - }); - } - - foreach (var filePath in Directory.GetFiles(targetPath)) - { - var fileInfo = new FileInfo(filePath); - - result.Files.Add(new ServiceModel.Types.File - { - Name = fileInfo.Name, - Extension = fileInfo.Extension, - FileSizeBytes = fileInfo.Length, - ModifiedDate = fileInfo.LastWriteTimeUtc, - IsTextFile = Config.TextFileExtensions.Contains(fileInfo.Extension), - }); - } - - return result; - } - - private FileInfo GetPath(Files request) - { - return new FileInfo(Path.Combine(this.Config.RootDirectory, request.Path.GetSafePath())); - } - - private FileInfo GetAndValidateExistingPath(Files request) - { - var targetFile = GetPath(request); - if (!targetFile.Exists && !Directory.Exists(targetFile.FullName)) - throw new HttpError(HttpStatusCode.NotFound, new FileNotFoundException("Could not find: " + request.Path)); - - return targetFile; - } - - private FileResult GetFileResult(FileInfo fileInfo) - { - var isTextFile = this.Config.TextFileExtensions.Contains(fileInfo.Extension); - - return new FileResult - { - Name = fileInfo.Name, - Extension = fileInfo.Extension, - FileSizeBytes = fileInfo.Length, - IsTextFile = isTextFile, - Contents = isTextFile ? File.ReadAllText(fileInfo.FullName) : null, - ModifiedDate = fileInfo.LastWriteTimeUtc, - }; - } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceInterface/RevertFilesService.cs b/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceInterface/RevertFilesService.cs deleted file mode 100644 index b911640..0000000 --- a/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceInterface/RevertFilesService.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.IO; -using System.Linq; -using RestFiles.ServiceModel; -using ServiceStack; - -namespace RestFiles.ServiceInterface -{ - /// - /// Define your ServiceStack web service request (i.e. Request DTO). - /// - public class RevertFilesService : Service - { - /// - /// Gets or sets the AppConfig. The built-in IoC used with ServiceStack autowires this property. - /// - public AppConfig Config { get; set; } - - public object Post(RevertFiles request) - { - var rootDir = Config.RootDirectory; - - if (Directory.Exists(rootDir)) - { - Directory.Delete(rootDir, true); - } - - CopyFiles(rootDir, "~/".MapHostAbsolutePath(), ".cs", ".htm", ".md"); - - var servicesDir = Path.Combine(rootDir, "services"); - CopyFiles(servicesDir, "~/../RestFiles.ServiceInterface/".MapHostAbsolutePath(), "Service.cs"); - - var testsDir = Path.Combine(rootDir, "tests"); - CopyFiles(testsDir, "~/../RestFiles.Tests/".MapHostAbsolutePath(), ".cs"); - - var dtosDir = Path.Combine(rootDir, "dtos"); - - var opsDtoPath = dtosDir; - CopyFiles(opsDtoPath, "~/../RestFiles.ServiceModel/".MapHostAbsolutePath()); - - var typesDtoPath = Path.Combine(dtosDir, "Types"); - CopyFiles(typesDtoPath, "~/../RestFiles.ServiceModel/Types/".MapHostAbsolutePath()); - - return new RevertFilesResponse(); - } - - private static void CopyFiles(string path, string filesPath, params string[] excludedFiles) - { - Directory.CreateDirectory(path); - var files = Directory.GetFiles(filesPath); - foreach (var file in files) - { - if (excludedFiles.IsEmpty() || excludedFiles.Any(x => file.EndsWith(x))) - { - var fileName = Path.GetFileName(file); - if (file.EndsWith(".cs")) - { - fileName += ".txt"; - } - File.Copy(file, Path.Combine(path, fileName)); - } - } - } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceModel/Types/File.cs b/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceModel/Types/File.cs deleted file mode 100644 index de717f8..0000000 --- a/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceModel/Types/File.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace RestFiles.ServiceModel.Types -{ - public class File - { - public string Name { get; set; } - public string Extension { get; set; } - public long FileSizeBytes { get; set; } - public DateTime ModifiedDate { get; set; } - public bool IsTextFile { get; set; } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceModel/Types/FileResult.cs b/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceModel/Types/FileResult.cs deleted file mode 100644 index 13a3634..0000000 --- a/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceModel/Types/FileResult.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace RestFiles.ServiceModel.Types -{ - public class FileResult - { - public string Name { get; set; } - public string Extension { get; set; } - public long FileSizeBytes { get; set; } - public DateTime ModifiedDate { get; set; } - public bool IsTextFile { get; set; } - public string Contents { get; set; } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceModel/Types/Folder.cs b/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceModel/Types/Folder.cs deleted file mode 100644 index 9594b3f..0000000 --- a/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceModel/Types/Folder.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace RestFiles.ServiceModel.Types -{ - public class Folder - { - public string Name { get; set; } - public DateTime ModifiedDate { get; set; } - public int FileCount { get; set; } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceModel/Types/FolderResult.cs b/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceModel/Types/FolderResult.cs deleted file mode 100644 index 2a0f551..0000000 --- a/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceModel/Types/FolderResult.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; - -namespace RestFiles.ServiceModel.Types -{ - public class FolderResult - { - public FolderResult() - { - Folders = new List(); - Files = new List(); - } - - public List Folders { get; set; } - public List Files { get; set; } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/src/RestFiles.Tests/Support/RestFilesHttpListener.cs b/src/RestFiles/RestFiles/App_Data/src/RestFiles.Tests/Support/RestFilesHttpListener.cs deleted file mode 100644 index 8050646..0000000 --- a/src/RestFiles/RestFiles/App_Data/src/RestFiles.Tests/Support/RestFilesHttpListener.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Funq; -using RestFiles.ServiceInterface; -using ServiceStack; - -namespace RestFiles.Tests -{ - public class RestFilesHttpListener - : AppHostHttpListenerBase - { - public const string ListeningOn = "http://localhost:8080/"; - - public RestFilesHttpListener() - : base("HttpListener Hosts for Unit Tests", typeof(FilesService).Assembly) { } - - public AppConfig Config { get; set; } - - public override void Configure(Container container) - { - this.Config = new AppConfig - { - RootDirectory = "~/App_Data/files/".MapAbsolutePath(), - TextFileExtensions = ".txt,.sln,.proj,.cs,.config,.asax".Split(','), - }; - container.Register(this.Config); - - this.Start(ListeningOn); - } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/src/RestFiles.Tests/SyncRestClientTests.cs b/src/RestFiles/RestFiles/App_Data/src/RestFiles.Tests/SyncRestClientTests.cs deleted file mode 100644 index 1025228..0000000 --- a/src/RestFiles/RestFiles/App_Data/src/RestFiles.Tests/SyncRestClientTests.cs +++ /dev/null @@ -1,227 +0,0 @@ -using System; -using System.IO; -using NUnit.Framework; -using RestFiles.ServiceModel; -using ServiceStack; - -/* For syntax highlighting and better readability of this file, view it on GitHub: - * https://github.com/ServiceStack/ServiceStack.Examples/blob/master/src/RestFiles/RestFiles.Tests/SyncRestClientTests.cs - */ - -namespace RestFiles.Tests -{ - /// - /// These test show how you can call ServiceStack REST web services synchronously using an IRestClient. - /// - /// Sync IO calls are the most familiar calling convention for .NET developers as it provides the simplest - /// API to use and requires the least code and effort as it allows you to program sequentially - /// - [TestFixture] - public class SyncRestClientTests - { - public const string WebServiceHostUrl = "http://localhost:8080/"; - private const string ReadmeFileContents = "THIS IS A README FILE"; - private const string ReplacedFileContents = "THIS README FILE HAS BEEN REPLACED"; - private const string TestUploadFileContents = "THIS FILE IS USED FOR UPLOADING IN TESTS"; - public string FilesRootDir; - - RestFilesHttpListener appHost; - - [TestFixtureSetUp] - public void TextFixtureSetUp() - { - appHost = new RestFilesHttpListener(); - appHost.Init(); - } - - [TestFixtureTearDown] - public void TestFixtureTearDown() - { - if (appHost != null) appHost.Dispose(); - appHost = null; - } - - [SetUp] - public void OnBeforeEachTest() - { - //Setup the files directory with some test files and folders - FilesRootDir = appHost.Config.RootDirectory; - if (Directory.Exists(FilesRootDir)) - { - Directory.Delete(FilesRootDir, true); - } - Directory.CreateDirectory(FilesRootDir + "SubFolder"); - Directory.CreateDirectory(FilesRootDir + "SubFolder2"); - File.WriteAllText(Path.Combine(FilesRootDir, "README.txt"), ReadmeFileContents); - File.WriteAllText(Path.Combine(FilesRootDir, "TESTUPLOAD.txt"), TestUploadFileContents); - } - - /// - /// Choose your favourite format to run tests with - /// - public IRestClient CreateRestClient() - { - return new JsonServiceClient(WebServiceHostUrl); //Best choice for Ajax web apps, 3x faster than XML - //return new XmlServiceClient(WebServiceHostUrl); //Ubiquitous structured data format best for supporting non .NET clients - //return new JsvServiceClient(WebServiceHostUrl); //Fastest, most compact and resilient format great for .NET to .NET client > server - } - - [Test] - public void Can_Get_to_retrieve_existing_file() - { - var restClient = CreateRestClient(); - - var response = restClient.Get("files/README.txt"); - - Assert.That(response.File.Contents, Is.EqualTo("THIS IS A README FILE")); - } - - [Test] - public void Can_Get_to_retrieve_existing_folder_listing() - { - var restClient = CreateRestClient(); - - var response = restClient.Get("files/"); - - Assert.That(response.Directory.Folders.Count, Is.EqualTo(2)); - Assert.That(response.Directory.Files.Count, Is.EqualTo(2)); - } - - [Test] - public void Can_Post_to_path_without_uploaded_files_to_create_a_new_Directory() - { - var restClient = CreateRestClient(); - - var response = restClient.Post("files/SubFolder/NewFolder", new Files()); - - Assert.That(Directory.Exists(FilesRootDir + "SubFolder/NewFolder")); - } - - [Test] - public void Can_WebRequest_POST_upload_file_to_save_new_file_and_create_new_Directory() - { - var restClient = CreateRestClient(); - - var fileToUpload = new FileInfo(FilesRootDir + "TESTUPLOAD.txt"); - - var response = restClient.PostFile("files/UploadedFiles/", - fileToUpload, MimeTypes.GetMimeType(fileToUpload.Name)); - - Assert.That(Directory.Exists(FilesRootDir + "UploadedFiles")); - Assert.That(File.ReadAllText(FilesRootDir + "UploadedFiles/TESTUPLOAD.txt"), - Is.EqualTo(TestUploadFileContents)); - } - - [Test] - public void Can_Put_to_replace_text_content_of_an_existing_file() - { - var restClient = CreateRestClient(); - - var response = restClient.Put(WebServiceHostUrl + "files/README.txt", - new Files { TextContents = ReplacedFileContents }); - - Assert.That(File.ReadAllText(FilesRootDir + "README.txt"), - Is.EqualTo(ReplacedFileContents)); - } - - [Test] - public void Can_Delete_to_replace_text_content_of_an_existing_file() - { - var restClient = CreateRestClient(); - - var response = restClient.Delete("files/README.txt"); - - Assert.That(!File.Exists(FilesRootDir + "README.txt")); - } - - - /* - * Error Handling Tests - */ - [Test] - public void GET_a_file_that_doesnt_exist_throws_a_404_FileNotFoundException() - { - var restClient = CreateRestClient(); - - try - { - var response = restClient.Get(WebServiceHostUrl + "files/UnknownFolder"); - - Assert.Fail("Should fail with 404 FileNotFoundException"); - } - catch (WebServiceException webEx) - { - Assert.That(webEx.StatusCode, Is.EqualTo(404)); - var response = (FilesResponse)webEx.ResponseDto; - Assert.That(response.ResponseStatus.ErrorCode, Is.EqualTo(typeof(FileNotFoundException).Name)); - Assert.That(response.ResponseStatus.Message, Is.EqualTo("Could not find: UnknownFolder")); - } - } - - [Test] - public void POST_to_an_existing_file_throws_a_500_NotSupportedException() - { - var restClient = CreateRestClient(); - - var fileToUpload = new FileInfo(FilesRootDir + "TESTUPLOAD.txt"); - - try - { - var response = restClient.PostFile(WebServiceHostUrl + "files/README.txt", - fileToUpload, MimeTypes.GetMimeType(fileToUpload.Name)); - - Assert.Fail("Should fail with NotSupportedException"); - } - catch (WebServiceException webEx) - { - Assert.That(webEx.StatusCode, Is.EqualTo(405)); - var response = (FilesResponse)webEx.ResponseDto; - Assert.That(response.ResponseStatus.ErrorCode, Is.EqualTo(typeof(NotSupportedException).Name)); - Assert.That(response.ResponseStatus.Message, - Is.EqualTo("POST only supports uploading new files. Use PUT to replace contents of an existing file")); - } - } - - [Test] - public void PUT_to_replace_a_non_existing_file_throws_404() - { - var restClient = CreateRestClient(); - - try - { - var response = restClient.Put(WebServiceHostUrl + "files/non-existing-file.txt", - new Files { TextContents = ReplacedFileContents }); - - Assert.Fail("Should fail with 404 FileNotFoundException"); - } - catch (WebServiceException webEx) - { - Assert.That(webEx.StatusCode, Is.EqualTo(404)); - var response = (FilesResponse)webEx.ResponseDto; - Assert.That(response.ResponseStatus.ErrorCode, Is.EqualTo(typeof(FileNotFoundException).Name)); - Assert.That(response.ResponseStatus.Message, Is.EqualTo("Could not find: non-existing-file.txt")); - } - } - - [Test] - public void DELETE_a_non_existing_file_throws_404() - { - var restClient = CreateRestClient(); - - try - { - var response = restClient.Delete(WebServiceHostUrl + "files/non-existing-file.txt"); - - Assert.Fail("Should fail with 404 FileNotFoundException"); - } - catch (WebServiceException webEx) - { - Assert.That(webEx.StatusCode, Is.EqualTo(404)); - var response = (FilesResponse)webEx.ResponseDto; - Assert.That(response.ResponseStatus.ErrorCode, Is.EqualTo(typeof(FileNotFoundException).Name)); - Assert.That(response.ResponseStatus.Message, Is.EqualTo("Could not find: non-existing-file.txt")); - } - } - - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/src/RestFiles.csproj b/src/RestFiles/RestFiles/App_Data/src/RestFiles.csproj deleted file mode 100644 index 134221f..0000000 --- a/src/RestFiles/RestFiles/App_Data/src/RestFiles.csproj +++ /dev/null @@ -1,183 +0,0 @@ - - - - - Debug - AnyCPU - 9.0.30729 - 2.0 - {70B9EEDE-BC2A-42EB-933D-A94D7D4275CB} - {349C5851-65DF-11DA-9384-00065B846F21};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - Library - Properties - RestFiles - RestFiles - v4.0 - - - 4.0 - - - true - - - - - - - - true - full - false - bin\ - DEBUG;TRACE - prompt - 4 - x86 - AllRules.ruleset - - - pdbonly - true - bin\ - TRACE - prompt - 4 - AllRules.ruleset - - - - False - ..\packages\ServiceStack.4.0.31\lib\net40\ServiceStack.dll - - - False - ..\packages\ServiceStack.Client.4.0.31\lib\net40\ServiceStack.Client.dll - - - False - ..\packages\ServiceStack.Common.4.0.31\lib\net40\ServiceStack.Common.dll - - - False - ..\packages\ServiceStack.Interfaces.4.0.31\lib\portable-wp80+sl5+net40+win8+monotouch+monoandroid\ServiceStack.Interfaces.dll - - - False - ..\packages\ServiceStack.Text.4.0.31\lib\net40\ServiceStack.Text.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Global.asax - - - - - - {6BBEDCA5-1183-49EE-AE80-0269DEB2EDEF} - RestFiles.ServiceModel - - - {5B91935A-7ED6-4496-871B-6AD25BC3F097} - RestFiles.ServiceInterface - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - - - - - - - True - True - 59436 - / - http://localhost:59436/ - False - False - - - False - - - - - - - - - - \ No newline at end of file diff --git a/src/RestFiles/RestFiles/Global.asax b/src/RestFiles/RestFiles/Global.asax deleted file mode 100644 index ac11b6c..0000000 --- a/src/RestFiles/RestFiles/Global.asax +++ /dev/null @@ -1 +0,0 @@ -<%@ Application Codebehind="Global.asax.cs" Inherits="RestFiles.Global" Language="C#" %> diff --git a/src/RestFiles/RestFiles/Global.asax.cs b/src/RestFiles/RestFiles/Global.asax.cs deleted file mode 100644 index 661072e..0000000 --- a/src/RestFiles/RestFiles/Global.asax.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.IO; -using Funq; -using RestFiles.ServiceInterface; -using ServiceStack; -using ServiceStack.Configuration; - -namespace RestFiles -{ - /// - /// Create your ServiceStack web service application with a singleton AppHost. - /// - public class AppHost : AppHostBase - { - /// - /// Initializes a new instance of your ServiceStack application, with the specified name and assembly containing the services. - /// - public AppHost() : base("REST Files", typeof(FilesService).Assembly) {} - - /// - /// Configure the container with the necessary routes for your ServiceStack application. - /// - /// The built-in IoC used with ServiceStack. - public override void Configure(Container container) - { - //Permit modern browsers (e.g. Firefox) to allow sending of any REST HTTP Method - Plugins.Add(new CorsFeature()); - - SetConfig(new HostConfig { - DebugMode = true, - }); - - var config = new AppConfig(new AppSettings()); - container.Register(config); - - if (!Directory.Exists(config.RootDirectory)) - Directory.CreateDirectory(config.RootDirectory); - } - } - - public class Global : System.Web.HttpApplication - { - protected void Application_Start(object sender, EventArgs e) - { - //Initialize your application - new AppHost().Init(); - } - } -} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/Properties/AssemblyInfo.cs b/src/RestFiles/RestFiles/Properties/AssemblyInfo.cs deleted file mode 100644 index 6148c36..0000000 --- a/src/RestFiles/RestFiles/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("ServiceStack.Examples.Host.Web")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Microsoft")] -[assembly: AssemblyProduct("ServiceStack.Examples.Host.Web")] -[assembly: AssemblyCopyright("Copyright © Microsoft 2009")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("3d5900ae-111a-45be-96b3-d9e4606ca793")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Revision and Build Numbers -// by using the '*' as shown below: -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/RestFiles/RestFiles/Properties/PublishProfiles/WebDeploy.pubxml b/src/RestFiles/RestFiles/Properties/PublishProfiles/WebDeploy.pubxml deleted file mode 100644 index 51587e1..0000000 --- a/src/RestFiles/RestFiles/Properties/PublishProfiles/WebDeploy.pubxml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - MSDeploy - Release - Any CPU - http://restfiles.servicestack.net - True - False - awstest.servicestack.net - RestFiles - - True - WMSVC - True - deploy - <_SavePWD>True - False - - \ No newline at end of file diff --git a/src/RestFiles/RestFiles/README.md b/src/RestFiles/RestFiles/README.md deleted file mode 100644 index dea9133..0000000 --- a/src/RestFiles/RestFiles/README.md +++ /dev/null @@ -1,52 +0,0 @@ -#The REST Files - -The Rest files is a ServiceStack Example project providing complete management of your remote filesystem, -over a REST-ful web services API, in a GitHub browser-like widget. - -## Client Info - -The client is written in [1 static default.html page, and uses only jQuery](https://github.com/ServiceStack/ServiceStack.Examples/blob/master/src/RestFiles/RestFiles/default.htm) -Because of the advanced HTML5 features used its best viewed in a modern browser (i.e. anything recent that's not IE) - - * CSS3 is used for folder browser animations - * HTML5 History State API is used for ajax state and page navigation - -## Server Info - -The /files service exposes a complete strong-typed REST-ful API, the entire implementation of which fits in only -[1 C# class](https://github.com/ServiceStack/ServiceStack.Examples/blob/master/src/RestFiles/RestFiles.ServiceInterface/FilesService.cs). - -As it was developed using the http://servicestack.net Open Source .NET/Mono Web Services Framework -it also able to expose this REST-ful API over a myraid of formats (with no extra code/config): - - * [json](http://servicestack.net/RestFiles/files/dtos/Types?format=json) - * [xml](http://servicestack.net/RestFiles/files/dtos/Types?format=xml) - * [jsv](http://servicestack.net/RestFiles/files/dtos/Types?format=jsv&debug=true) - * [csv](http://servicestack.net/RestFiles/files/dtos/Types?format=csv) - -*Note: The speed of web services are faster than what they appear, as the delay + animations are for - gratuitous purposes only :) All but the xml format uses the high-performance cross-platform, - serializers in ServiceStack.Text. e.g the JsonSerializer serializer used is over 3.6x faster - that the fastest JSON Serialzer shipped with .NET, see: - [the Northwind Benchmarks](http://www.servicestack.net/benchmarks/NorthwindDatabaseRowsSerialization.100000-times.2010-08-17.html) - -SOAP 1.1/1.2 endpoints are also available at the following url: - - * [soap11](http://servicestack.net/RestFiles/servicestack/soap11) - * [soap12](http://servicestack.net/RestFiles/servicestack/soap12) - -As a result of the strong-typed DTO pattern used to define the the webservice, ServiceStack is able to -generate the xsds, wsdls, metadata documentation on the fly at: - - * [docs](http://servicestack.net/RestFiles/servicestack/metadata) - * [xsd](http://servicestack.net/RestFiles/servicestack/metadata?xsd=1) - * [wsdl](http://servicestack.net/RestFiles/servicestack/soap12) (HTTP GET) - -Caveat: XmlSchema is not fully implemented as of MONO <= 2.8, so in many cases you will need to -host your service on a Windows/.NET server to view your web services XSD and WSDLS. - -## Live Demo - -The live demo is hosted on Linux (Cent OS) / Nginx using [MONO](http://www.mono-project.com) - -*Not affiliated with GitHub, which is a trademark of GitHub Inc. diff --git a/src/RestFiles/RestFiles/RestFiles.csproj b/src/RestFiles/RestFiles/RestFiles.csproj deleted file mode 100644 index 1f11944..0000000 --- a/src/RestFiles/RestFiles/RestFiles.csproj +++ /dev/null @@ -1,201 +0,0 @@ - - - - - Debug - AnyCPU - 9.0.30729 - 2.0 - {70B9EEDE-BC2A-42EB-933D-A94D7D4275CB} - {349C5851-65DF-11DA-9384-00065B846F21};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - Library - Properties - RestFiles - RestFiles - v4.5 - - - 4.0 - - - true - - - - - - - - - true - full - false - bin\ - DEBUG;TRACE - prompt - 4 - x86 - AllRules.ruleset - false - - - pdbonly - true - bin\ - TRACE - prompt - 4 - AllRules.ruleset - false - - - - ..\packages\ServiceStack.4.5.0\lib\net45\ServiceStack.dll - True - - - ..\packages\ServiceStack.Client.4.5.0\lib\net45\ServiceStack.Client.dll - True - - - ..\packages\ServiceStack.Common.4.5.0\lib\net45\ServiceStack.Common.dll - True - - - ..\packages\ServiceStack.Interfaces.4.5.0\lib\portable-wp80+sl5+net45+win8+wpa81+monotouch+monoandroid+xamarin.ios10\ServiceStack.Interfaces.dll - True - - - ..\packages\ServiceStack.Text.4.5.0\lib\net45\ServiceStack.Text.dll - True - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Global.asax - - - - - - {6BBEDCA5-1183-49EE-AE80-0269DEB2EDEF} - RestFiles.ServiceModel - - - {5B91935A-7ED6-4496-871B-6AD25BC3F097} - RestFiles.ServiceInterface - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - - - - - - - True - True - 59436 - / - http://localhost:59436/ - False - False - - - False - - - - - - - - - - \ No newline at end of file diff --git a/src/RestFiles/RestFiles/Web.config b/src/RestFiles/RestFiles/Web.config deleted file mode 100644 index fa80c89..0000000 --- a/src/RestFiles/RestFiles/Web.config +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/RestFiles/RestFiles/crossdomain.xml b/src/RestFiles/RestFiles/crossdomain.xml deleted file mode 100644 index 0d42929..0000000 --- a/src/RestFiles/RestFiles/crossdomain.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/RestFiles/RestFiles/packages.config b/src/RestFiles/RestFiles/packages.config deleted file mode 100644 index 6b7d575..0000000 --- a/src/RestFiles/RestFiles/packages.config +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/RestFiles/Startup.cs b/src/RestFiles/Startup.cs new file mode 100644 index 0000000..477607c --- /dev/null +++ b/src/RestFiles/Startup.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Funq; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using RestFiles.ServiceInterface; +using ServiceStack; +using ServiceStack.Configuration; + +namespace RestFiles +{ + public class Startup + { + IConfiguration Configuration { get; set; } + public Startup(IConfiguration configuration) => Configuration = configuration; + + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseServiceStack(new AppHost { + AppSettings = new NetCoreAppSettings(Configuration) + }); + } + } + + public class AppHost : AppHostBase + { + /// + /// Initializes a new instance of your ServiceStack application, with the specified name and assembly containing the services. + /// + public AppHost() : base("REST Files", typeof(FilesService).Assembly) { } + + /// + /// Configure the container with the necessary routes for your ServiceStack application. + /// + /// The built-in IoC used with ServiceStack. + public override void Configure(Container container) + { + //Permit modern browsers (e.g. Firefox) to allow sending of any REST HTTP Method + Plugins.Add(new CorsFeature()); + + SetConfig(new HostConfig + { + UseCamelCase = false, + }); + + var config = new AppConfig + { + RootDirectory = AppSettings.GetString("RootDirectory"), + TextFileExtensions = AppSettings.GetList("TextFileExtensions").ToList(), + ExcludeDirectories = AppSettings.GetList("ExcludeDirectories").ToList(), + }; + container.Register(config); + + if (!Directory.Exists(config.RootDirectory)) + Directory.CreateDirectory(config.RootDirectory); + + Plugins.Add(new SharpPagesFeature { + MetadataDebugAdminRole = RoleNames.AllowAnon, + Args = { ["config"] = config } + }); + } + } +} diff --git a/src/RestFiles/appsettings.Development.json b/src/RestFiles/appsettings.Development.json new file mode 100644 index 0000000..f8c60cf --- /dev/null +++ b/src/RestFiles/appsettings.Development.json @@ -0,0 +1,20 @@ +{ + "DebugMode": true, + "Logging": { + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "Console": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } + } +} diff --git a/src/RestFiles/appsettings.json b/src/RestFiles/appsettings.json new file mode 100644 index 0000000..2ff7e67 --- /dev/null +++ b/src/RestFiles/appsettings.json @@ -0,0 +1,34 @@ +{ + "RootDirectory": "wwwroot/files/", + "ExcludeDirectories": [ + "bin", + "Properties" + ], + "TextFileExtensions": [ + "txt", + "sln", + "proj", + "cs", + "config", + "asax", + "css", + "htm", + "html", + "xml", + "js", + "md" + ], + "Logging": { + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Warning" + } + }, + "Console": { + "LogLevel": { + "Default": "Warning" + } + } + } +} diff --git a/src/RestFiles/RestFiles/Content/Css/default.css b/src/RestFiles/wwwroot/Content/Css/default.css similarity index 100% rename from src/RestFiles/RestFiles/Content/Css/default.css rename to src/RestFiles/wwwroot/Content/Css/default.css diff --git a/src/RestFiles/RestFiles/Content/Images/1x1black-50a.png b/src/RestFiles/wwwroot/Content/Images/1x1black-50a.png similarity index 100% rename from src/RestFiles/RestFiles/Content/Images/1x1black-50a.png rename to src/RestFiles/wwwroot/Content/Images/1x1black-50a.png diff --git a/src/RestFiles/RestFiles/Content/Images/Chrome_logo.png b/src/RestFiles/wwwroot/Content/Images/Chrome_logo.png similarity index 100% rename from src/RestFiles/RestFiles/Content/Images/Chrome_logo.png rename to src/RestFiles/wwwroot/Content/Images/Chrome_logo.png diff --git a/src/RestFiles/RestFiles/Content/Images/Mozilla_logo.png b/src/RestFiles/wwwroot/Content/Images/Mozilla_logo.png similarity index 100% rename from src/RestFiles/RestFiles/Content/Images/Mozilla_logo.png rename to src/RestFiles/wwwroot/Content/Images/Mozilla_logo.png diff --git a/src/RestFiles/RestFiles/Content/Images/Opera_logo.png b/src/RestFiles/wwwroot/Content/Images/Opera_logo.png similarity index 100% rename from src/RestFiles/RestFiles/Content/Images/Opera_logo.png rename to src/RestFiles/wwwroot/Content/Images/Opera_logo.png diff --git a/src/RestFiles/RestFiles/Content/Images/Safari_logo.jpg b/src/RestFiles/wwwroot/Content/Images/Safari_logo.jpg similarity index 100% rename from src/RestFiles/RestFiles/Content/Images/Safari_logo.jpg rename to src/RestFiles/wwwroot/Content/Images/Safari_logo.jpg diff --git a/src/RestFiles/RestFiles/Content/Images/bg_gradient.gif b/src/RestFiles/wwwroot/Content/Images/bg_gradient.gif similarity index 100% rename from src/RestFiles/RestFiles/Content/Images/bg_gradient.gif rename to src/RestFiles/wwwroot/Content/Images/bg_gradient.gif diff --git a/src/RestFiles/wwwroot/Content/Images/btn-github.png b/src/RestFiles/wwwroot/Content/Images/btn-github.png new file mode 100644 index 0000000..e7605b0 Binary files /dev/null and b/src/RestFiles/wwwroot/Content/Images/btn-github.png differ diff --git a/src/RestFiles/RestFiles/Content/Images/close-icon.png b/src/RestFiles/wwwroot/Content/Images/close-icon.png similarity index 100% rename from src/RestFiles/RestFiles/Content/Images/close-icon.png rename to src/RestFiles/wwwroot/Content/Images/close-icon.png diff --git a/src/RestFiles/RestFiles/Content/Images/delete_icon.png b/src/RestFiles/wwwroot/Content/Images/delete_icon.png similarity index 100% rename from src/RestFiles/RestFiles/Content/Images/delete_icon.png rename to src/RestFiles/wwwroot/Content/Images/delete_icon.png diff --git a/src/RestFiles/RestFiles/Content/Images/dir.png b/src/RestFiles/wwwroot/Content/Images/dir.png similarity index 100% rename from src/RestFiles/RestFiles/Content/Images/dir.png rename to src/RestFiles/wwwroot/Content/Images/dir.png diff --git a/src/RestFiles/RestFiles/Content/Images/file_head.gif b/src/RestFiles/wwwroot/Content/Images/file_head.gif similarity index 100% rename from src/RestFiles/RestFiles/Content/Images/file_head.gif rename to src/RestFiles/wwwroot/Content/Images/file_head.gif diff --git a/src/RestFiles/RestFiles/Content/Images/ie8_logo.jpg b/src/RestFiles/wwwroot/Content/Images/ie8_logo.jpg similarity index 100% rename from src/RestFiles/RestFiles/Content/Images/ie8_logo.jpg rename to src/RestFiles/wwwroot/Content/Images/ie8_logo.jpg diff --git a/src/RestFiles/RestFiles/Content/Images/public.png b/src/RestFiles/wwwroot/Content/Images/public.png similarity index 100% rename from src/RestFiles/RestFiles/Content/Images/public.png rename to src/RestFiles/wwwroot/Content/Images/public.png diff --git a/src/RestFiles/RestFiles/Content/Images/rip-ie6-300.jpg b/src/RestFiles/wwwroot/Content/Images/rip-ie6-300.jpg similarity index 100% rename from src/RestFiles/RestFiles/Content/Images/rip-ie6-300.jpg rename to src/RestFiles/wwwroot/Content/Images/rip-ie6-300.jpg diff --git a/src/RestFiles/RestFiles/Content/Images/row_bg.png b/src/RestFiles/wwwroot/Content/Images/row_bg.png similarity index 100% rename from src/RestFiles/RestFiles/Content/Images/row_bg.png rename to src/RestFiles/wwwroot/Content/Images/row_bg.png diff --git a/src/RestFiles/RestFiles/Content/Images/txt.png b/src/RestFiles/wwwroot/Content/Images/txt.png similarity index 100% rename from src/RestFiles/RestFiles/Content/Images/txt.png rename to src/RestFiles/wwwroot/Content/Images/txt.png diff --git a/src/RestFiles/RestFiles/App_Data/files/README.md b/src/RestFiles/wwwroot/README.md similarity index 100% rename from src/RestFiles/RestFiles/App_Data/files/README.md rename to src/RestFiles/wwwroot/README.md diff --git a/src/RestFiles/RestFiles/Scripts/ajaxfileupload.js b/src/RestFiles/wwwroot/Scripts/ajaxfileupload.js similarity index 100% rename from src/RestFiles/RestFiles/Scripts/ajaxfileupload.js rename to src/RestFiles/wwwroot/Scripts/ajaxfileupload.js diff --git a/src/RestFiles/RestFiles/Scripts/jquery-1.8.3.min.js b/src/RestFiles/wwwroot/Scripts/jquery-1.8.3.min.js similarity index 100% rename from src/RestFiles/RestFiles/Scripts/jquery-1.8.3.min.js rename to src/RestFiles/wwwroot/Scripts/jquery-1.8.3.min.js diff --git a/src/RestFiles/RestFiles/default.htm b/src/RestFiles/wwwroot/default.htm similarity index 94% rename from src/RestFiles/RestFiles/default.htm rename to src/RestFiles/wwwroot/default.htm index 163bb54..b203ba5 100644 --- a/src/RestFiles/RestFiles/default.htm +++ b/src/RestFiles/wwwroot/default.htm @@ -8,11 +8,11 @@ - ServiceStack Home

@@ -25,19 +25,19 @@

GitHub-like browser with complete remote file management over REST in - + 1 page jQuery + - + 1 page C#

@@ -45,7 +45,7 @@

The REST /files web service API:

-
GET /path/to/folder
+
GET /path/to/folder
Directory listing at server
GET /path/to/file.txt
Info and contents of text file at path
@@ -66,8 +66,6 @@

diff --git a/src/RestFiles/RestFiles/App_Data/src/Css/default.css b/src/RestFiles/wwwroot/src/Css/default.css similarity index 100% rename from src/RestFiles/RestFiles/App_Data/src/Css/default.css rename to src/RestFiles/wwwroot/src/Css/default.css diff --git a/src/RestFiles/RestFiles/App_Data/src/README.md b/src/RestFiles/wwwroot/src/README.md similarity index 100% rename from src/RestFiles/RestFiles/App_Data/src/README.md rename to src/RestFiles/wwwroot/src/README.md diff --git a/src/RestFiles/wwwroot/src/RestFiles.ServiceInterface/AppConfig.cs b/src/RestFiles/wwwroot/src/RestFiles.ServiceInterface/AppConfig.cs new file mode 100644 index 0000000..219c606 --- /dev/null +++ b/src/RestFiles/wwwroot/src/RestFiles.ServiceInterface/AppConfig.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace RestFiles.ServiceInterface +{ + public class AppConfig + { + public string RootDirectory { get; set; } + + public List TextFileExtensions { get; set; } + + public List ExcludeDirectories { get; set; } + } +} \ No newline at end of file diff --git a/src/RestFiles/wwwroot/src/RestFiles.ServiceInterface/FilesService.cs b/src/RestFiles/wwwroot/src/RestFiles.ServiceInterface/FilesService.cs new file mode 100644 index 0000000..b390c30 --- /dev/null +++ b/src/RestFiles/wwwroot/src/RestFiles.ServiceInterface/FilesService.cs @@ -0,0 +1,150 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using RestFiles.ServiceModel; +using RestFiles.ServiceModel.Types; +using ServiceStack; +using ServiceStack.VirtualPath; + +namespace RestFiles.ServiceInterface +{ + /// + /// Define your ServiceStack web service request (i.e. Request DTO). + /// + public class FilesService : Service + { + public AppConfig Config { get; set; } + + public object Get(Files request) + { + var targetPath = GetAndValidateExistingPath(request); + + var isDirectory = VirtualFiles.IsDirectory(targetPath); + + if (!isDirectory && request.ForDownload) + return new HttpResult(VirtualFiles.GetFile(targetPath), asAttachment: true); + + var response = isDirectory + ? new FilesResponse { Directory = GetFolderResult(targetPath) } + : new FilesResponse { File = GetFileResult(targetPath) }; + + return response; + } + + public object Post(Files request) + { + var targetDir = GetPath(request); + + if (VirtualFiles.IsFile(targetDir)) + throw new NotSupportedException( + "POST only supports uploading new files. Use PUT to replace contents of an existing file"); + + foreach (var uploadedFile in base.Request.Files) + { + var newFilePath = targetDir.CombineWith(uploadedFile.FileName); + VirtualFiles.WriteFile(newFilePath, uploadedFile.InputStream); + } + + return new FilesResponse(); + } + + public void Put(Files request) + { + var targetFile = VirtualFiles.GetFile(GetAndValidateExistingPath(request)); + + if (!Config.TextFileExtensions.Contains(targetFile.Extension)) + throw new NotSupportedException("PUT Can only update text files, not: " + targetFile.Extension); + + if (request.TextContents == null) + throw new ArgumentNullException("TextContents"); + + VirtualFiles.WriteFile(targetFile.VirtualPath, request.TextContents); + } + + public void Delete(Files request) + { + var targetFile = GetAndValidateExistingPath(request); + VirtualFiles.DeleteFile(targetFile); + } + + private FolderResult GetFolderResult(string targetPath) + { + var result = new FolderResult(); + + var dir = VirtualFiles.GetDirectory(targetPath); + foreach (var subDir in dir.Directories) + { + if (Config.ExcludeDirectories.Contains(subDir.Name)) continue; + + result.Folders.Add(new Folder + { + Name = subDir.Name, + ModifiedDate = subDir.LastModified, + FileCount = subDir.GetFiles().Count(), + }); + } + + foreach (var fileInfo in dir.GetFiles()) + { + result.Files.Add(new ServiceModel.Types.File + { + Name = fileInfo.Name, + Extension = fileInfo.Extension, + FileSizeBytes = fileInfo.Length, + ModifiedDate = fileInfo.LastModified, + IsTextFile = Config.TextFileExtensions.Contains(fileInfo.Extension), + }); + } + + return result; + } + + private string GetPath(Files request) + { + return Config.RootDirectory.CombineWith(GetSafePath(request.Path)); + } + + private string GetAndValidateExistingPath(Files request) + { + var targetPath = GetPath(request); + if (!VirtualFiles.IsFile(targetPath) && !VirtualFiles.IsDirectory(targetPath)) + throw new HttpError(HttpStatusCode.NotFound, new FileNotFoundException("Could not find: " + request.Path)); + + return targetPath; + } + + private FileResult GetFileResult(string filePath) + { + var file = VirtualFiles.GetFile(filePath); + var isTextFile = Config.TextFileExtensions.Contains(file.Extension); + + return new FileResult + { + Name = file.Name, + Extension = file.Extension, + FileSizeBytes = file.Length, + IsTextFile = isTextFile, + Contents = isTextFile ? VirtualFiles.GetFile(file.VirtualPath).ReadAllText() : null, + ModifiedDate = file.LastModified, + }; + } + + public static string GetSafePath(string filePath) + { + if (string.IsNullOrEmpty(filePath)) return string.Empty; + + //Strip invalid chars + foreach (var invalidChar in Path.GetInvalidPathChars()) + { + filePath = filePath.Replace(invalidChar.ToString(), string.Empty); + } + + return filePath + .TrimStart('.', '/', '\\') //Remove illegal chars at the start + .Replace('\\', '/') //Switch all to use the same seperator + .Replace("../", string.Empty) //Remove access to top-level directories anywhere else + .Replace('/', Path.DirectorySeparatorChar); //Switch all to use the OS seperator + } + } +} \ No newline at end of file diff --git a/src/RestFiles/wwwroot/src/RestFiles.ServiceInterface/RestFiles.ServiceInterface.csproj b/src/RestFiles/wwwroot/src/RestFiles.ServiceInterface/RestFiles.ServiceInterface.csproj new file mode 100644 index 0000000..147c9c5 --- /dev/null +++ b/src/RestFiles/wwwroot/src/RestFiles.ServiceInterface/RestFiles.ServiceInterface.csproj @@ -0,0 +1,26 @@ + + + + netstandard1.6 + RestFiles.ServiceInterface + RestFiles.ServiceInterface + 1.6.0 + $(PackageTargetFallback);dnxcore50 + false + false + false + + + + + + + + + + + + + + + diff --git a/src/RestFiles/wwwroot/src/RestFiles.ServiceInterface/RevertFilesService.cs b/src/RestFiles/wwwroot/src/RestFiles.ServiceInterface/RevertFilesService.cs new file mode 100644 index 0000000..008a337 --- /dev/null +++ b/src/RestFiles/wwwroot/src/RestFiles.ServiceInterface/RevertFilesService.cs @@ -0,0 +1,29 @@ +using RestFiles.ServiceModel; +using ServiceStack; +using ServiceStack.VirtualPath; + +namespace RestFiles.ServiceInterface +{ + /// + /// Define your ServiceStack web service request (i.e. Request DTO). + /// + public class RevertFilesService : Service + { + /// + /// Gets or sets the AppConfig. The built-in IoC used with ServiceStack autowires this property. + /// + public AppConfig Config { get; set; } + + public object Post(RevertFiles request) + { + VirtualFiles.DeleteFolder(Config.RootDirectory); + + foreach (var file in VirtualFiles.GetDirectory("src").GetAllMatchingFiles("*.*")) + { + VirtualFiles.WriteFile(file, file.VirtualPath.Replace("src/", Config.RootDirectory)); + } + + return new RevertFilesResponse(); + } + } +} \ No newline at end of file diff --git a/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceInterface/Support/FileExtensions.cs b/src/RestFiles/wwwroot/src/RestFiles.ServiceInterface/Support/FileExtensions.cs similarity index 100% rename from src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceInterface/Support/FileExtensions.cs rename to src/RestFiles/wwwroot/src/RestFiles.ServiceInterface/Support/FileExtensions.cs diff --git a/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceModel/Files.cs b/src/RestFiles/wwwroot/src/RestFiles.ServiceModel/Files.cs similarity index 100% rename from src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceModel/Files.cs rename to src/RestFiles/wwwroot/src/RestFiles.ServiceModel/Files.cs diff --git a/src/RestFiles/wwwroot/src/RestFiles.ServiceModel/RestFiles.ServiceModel.csproj b/src/RestFiles/wwwroot/src/RestFiles.ServiceModel/RestFiles.ServiceModel.csproj new file mode 100644 index 0000000..404aefe --- /dev/null +++ b/src/RestFiles/wwwroot/src/RestFiles.ServiceModel/RestFiles.ServiceModel.csproj @@ -0,0 +1,18 @@ + + + + netstandard1.6 + RestFiles.ServiceModel + RestFiles.ServiceModel + 1.6.0 + $(PackageTargetFallback);dnxcore50 + false + false + false + + + + + + + diff --git a/src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceModel/RevertFiles.cs b/src/RestFiles/wwwroot/src/RestFiles.ServiceModel/RevertFiles.cs similarity index 100% rename from src/RestFiles/RestFiles/App_Data/src/RestFiles.ServiceModel/RevertFiles.cs rename to src/RestFiles/wwwroot/src/RestFiles.ServiceModel/RevertFiles.cs diff --git a/src/RestFiles/wwwroot/src/RestFiles.ServiceModel/Types/File.cs b/src/RestFiles/wwwroot/src/RestFiles.ServiceModel/Types/File.cs new file mode 100644 index 0000000..08e6318 --- /dev/null +++ b/src/RestFiles/wwwroot/src/RestFiles.ServiceModel/Types/File.cs @@ -0,0 +1,13 @@ +using System; + +namespace RestFiles.ServiceModel.Types +{ + public class File + { + public string Name { get; set; } + public string Extension { get; set; } + public long FileSizeBytes { get; set; } + public DateTime ModifiedDate { get; set; } + public bool IsTextFile { get; set; } + } +} \ No newline at end of file diff --git a/src/RestFiles/wwwroot/src/RestFiles.ServiceModel/Types/FileResult.cs b/src/RestFiles/wwwroot/src/RestFiles.ServiceModel/Types/FileResult.cs new file mode 100644 index 0000000..da113f5 --- /dev/null +++ b/src/RestFiles/wwwroot/src/RestFiles.ServiceModel/Types/FileResult.cs @@ -0,0 +1,14 @@ +using System; + +namespace RestFiles.ServiceModel.Types +{ + public class FileResult + { + public string Name { get; set; } + public string Extension { get; set; } + public long FileSizeBytes { get; set; } + public DateTime ModifiedDate { get; set; } + public bool IsTextFile { get; set; } + public string Contents { get; set; } + } +} \ No newline at end of file diff --git a/src/RestFiles/wwwroot/src/RestFiles.ServiceModel/Types/Folder.cs b/src/RestFiles/wwwroot/src/RestFiles.ServiceModel/Types/Folder.cs new file mode 100644 index 0000000..46a7580 --- /dev/null +++ b/src/RestFiles/wwwroot/src/RestFiles.ServiceModel/Types/Folder.cs @@ -0,0 +1,11 @@ +using System; + +namespace RestFiles.ServiceModel.Types +{ + public class Folder + { + public string Name { get; set; } + public DateTime ModifiedDate { get; set; } + public int FileCount { get; set; } + } +} \ No newline at end of file diff --git a/src/RestFiles/wwwroot/src/RestFiles.ServiceModel/Types/FolderResult.cs b/src/RestFiles/wwwroot/src/RestFiles.ServiceModel/Types/FolderResult.cs new file mode 100644 index 0000000..a8f9247 --- /dev/null +++ b/src/RestFiles/wwwroot/src/RestFiles.ServiceModel/Types/FolderResult.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace RestFiles.ServiceModel.Types +{ + public class FolderResult + { + public FolderResult() + { + Folders = new List(); + Files = new List(); + } + + public List Folders { get; set; } + public List Files { get; set; } + } +} \ No newline at end of file diff --git a/src/RestFiles/RestFiles.Tests/AsyncRestClientTests.cs b/src/RestFiles/wwwroot/src/RestFiles.Tests/AsyncRestClientTests.cs similarity index 96% rename from src/RestFiles/RestFiles.Tests/AsyncRestClientTests.cs rename to src/RestFiles/wwwroot/src/RestFiles.Tests/AsyncRestClientTests.cs index 7ac23c4..ba6ef8e 100644 --- a/src/RestFiles/RestFiles.Tests/AsyncRestClientTests.cs +++ b/src/RestFiles/wwwroot/src/RestFiles.Tests/AsyncRestClientTests.cs @@ -27,20 +27,19 @@ public class AsyncRestClientTests private const string TestUploadFileContents = "THIS FILE IS USED FOR UPLOADING IN TESTS"; public string FilesRootDir; - RestFilesHttpListener appHost; + TestAppHost appHost; - [TestFixtureSetUp] + [OneTimeSetUp] public void TextFixtureSetUp() { - appHost = new RestFilesHttpListener(); + appHost = new TestAppHost(); appHost.Init(); } - [TestFixtureTearDown] + [OneTimeTearDown] public void TestFixtureTearDown() { - if (appHost != null) appHost.Dispose(); - appHost = null; + appHost?.Dispose(); } [SetUp] @@ -106,7 +105,10 @@ public void Can_WebRequest_POST_upload_file_to_save_new_file_and_create_new_Dire var webRequest = WebRequest.Create(WebServiceHostUrl + "files/UploadedFiles/"); var fileToUpload = new FileInfo(FilesRootDir + "TESTUPLOAD.txt"); - webRequest.UploadFile(fileToUpload, MimeTypes.GetMimeType(fileToUpload.Name)); + using (var stream = fileToUpload.OpenRead()) + { + webRequest.UploadFile(stream, MimeTypes.GetMimeType(fileToUpload.Name)); + } Assert.That(Directory.Exists(FilesRootDir + "UploadedFiles")); Assert.That(File.ReadAllText(FilesRootDir + "UploadedFiles/TESTUPLOAD.txt"), diff --git a/src/RestFiles/wwwroot/src/RestFiles.Tests/RestFiles.Tests.csproj b/src/RestFiles/wwwroot/src/RestFiles.Tests/RestFiles.Tests.csproj new file mode 100644 index 0000000..5db74c4 --- /dev/null +++ b/src/RestFiles/wwwroot/src/RestFiles.Tests/RestFiles.Tests.csproj @@ -0,0 +1,30 @@ + + + + netcoreapp2.0 + RestFiles.Tests + Library + RestFiles.Tests + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/RestFiles/RestFiles/App_Data/files/tests/AsyncRestClientTests.cs.txt b/src/RestFiles/wwwroot/src/RestFiles.Tests/SyncRestClientTests.cs similarity index 57% rename from src/RestFiles/RestFiles/App_Data/files/tests/AsyncRestClientTests.cs.txt rename to src/RestFiles/wwwroot/src/RestFiles.Tests/SyncRestClientTests.cs index 7ac23c4..95204b4 100644 --- a/src/RestFiles/RestFiles/App_Data/files/tests/AsyncRestClientTests.cs.txt +++ b/src/RestFiles/wwwroot/src/RestFiles.Tests/SyncRestClientTests.cs @@ -1,25 +1,28 @@ using System; using System.IO; -using System.Net; -using System.Threading.Tasks; +using System.Linq; +using Funq; using NUnit.Framework; -using RestFiles.ServiceModel; +using RestFiles.ServiceInterface; using ServiceStack; +using File = System.IO.File; +using Files = RestFiles.ServiceModel.Files; +using FilesResponse = RestFiles.ServiceModel.FilesResponse; /* For syntax highlighting and better readability of this file, view it on GitHub: - * https://github.com/ServiceStack/ServiceStack.Examples/blob/master/src/RestFiles/RestFiles.Tests/AsyncRestClientTests.cs + * https://github.com/ServiceStack/ServiceStack.Examples/blob/master/src/RestFiles/RestFiles.Tests/SyncRestClientTests.cs */ namespace RestFiles.Tests { /// - /// These test show how you can call ServiceStack REST web services asynchronously using an IRestClientAsync. + /// These test show how you can call ServiceStack REST web services synchronously using an IRestClient. /// - /// Async service calls are a great for GUI apps as they can be called without blocking the UI thread. - /// They are also great for performance as no time is spent on blocking IO calls. + /// Sync IO calls are the most familiar calling convention for .NET developers as it provides the simplest + /// API to use and requires the least code and effort as it allows you to program sequentially /// [TestFixture] - public class AsyncRestClientTests + public class SyncRestClientTests { public const string WebServiceHostUrl = "http://localhost:8080/"; private const string ReadmeFileContents = "THIS IS A README FILE"; @@ -27,25 +30,26 @@ public class AsyncRestClientTests private const string TestUploadFileContents = "THIS FILE IS USED FOR UPLOADING IN TESTS"; public string FilesRootDir; - RestFilesHttpListener appHost; + TestAppHost appHost; - [TestFixtureSetUp] + [OneTimeSetUp] public void TextFixtureSetUp() { - appHost = new RestFilesHttpListener(); + appHost = new TestAppHost(); appHost.Init(); } - [TestFixtureTearDown] + [OneTimeTearDown] public void TestFixtureTearDown() { - if (appHost != null) appHost.Dispose(); + appHost?.Dispose(); appHost = null; } [SetUp] public void OnBeforeEachTest() { + //Setup the files directory with some test files and folders FilesRootDir = appHost.Config.RootDirectory; if (Directory.Exists(FilesRootDir)) { @@ -57,45 +61,43 @@ public void OnBeforeEachTest() File.WriteAllText(Path.Combine(FilesRootDir, "TESTUPLOAD.txt"), TestUploadFileContents); } - public IRestClientAsync CreateAsyncRestClient() + /// + /// Choose your favourite format to run tests with + /// + public IRestClient CreateRestClient() { - return new JsonServiceClient(WebServiceHostUrl); //Best choice for Ajax web apps, faster than XML - //return new XmlServiceClient(WebServiceHostUrl); //Ubiquitous structured data format best for supporting non .NET clients - //return new JsvServiceClient(WebServiceHostUrl); //Fastest, most compact and resilient format great for .NET to .NET client / server - } - - private static void FailOnAsyncError(T response, Exception ex) - { - Assert.Fail(ex.Message); + return new JsonServiceClient(WebServiceHostUrl); //Best choice for Ajax web apps, 3x faster than XML + //return new XmlServiceClient(WebServiceHostUrl); //Ubiquitous structured data format best for supporting non .NET clients + //return new JsvServiceClient(WebServiceHostUrl); //Fastest, most compact and resilient format great for .NET to .NET client > server } [Test] - public async Task Can_GetAsync_to_retrieve_existing_file() + public void Can_Get_to_retrieve_existing_file() { - var restClient = CreateAsyncRestClient(); + var restClient = CreateRestClient(); - var response = await restClient.GetAsync("files/README.txt"); + var response = restClient.Get("files/README.txt"); Assert.That(response.File.Contents, Is.EqualTo("THIS IS A README FILE")); } [Test] - public async Task Can_GetAsync_to_retrieve_existing_folder_listing() + public void Can_Get_to_retrieve_existing_folder_listing() { - var restClient = CreateAsyncRestClient(); + var restClient = CreateRestClient(); - var response = await restClient.GetAsync("files/"); + var response = restClient.Get("files/"); Assert.That(response.Directory.Folders.Count, Is.EqualTo(2)); Assert.That(response.Directory.Files.Count, Is.EqualTo(2)); } [Test] - public async Task Can_PostAsync_to_path_without_uploaded_files_to_create_a_new_Directory() + public void Can_Post_to_path_without_uploaded_files_to_create_a_new_Directory() { - var restClient = CreateAsyncRestClient(); + var restClient = CreateRestClient(); - FilesResponse response = await restClient.PostAsync("files/SubFolder/NewFolder", new Files()); + var response = restClient.Post("files/SubFolder/NewFolder", new Files()); Assert.That(Directory.Exists(FilesRootDir + "SubFolder/NewFolder")); } @@ -103,23 +105,11 @@ public async Task Can_PostAsync_to_path_without_uploaded_files_to_create_a_new_D [Test] public void Can_WebRequest_POST_upload_file_to_save_new_file_and_create_new_Directory() { - var webRequest = WebRequest.Create(WebServiceHostUrl + "files/UploadedFiles/"); + var restClient = CreateRestClient(); var fileToUpload = new FileInfo(FilesRootDir + "TESTUPLOAD.txt"); - webRequest.UploadFile(fileToUpload, MimeTypes.GetMimeType(fileToUpload.Name)); - - Assert.That(Directory.Exists(FilesRootDir + "UploadedFiles")); - Assert.That(File.ReadAllText(FilesRootDir + "UploadedFiles/TESTUPLOAD.txt"), - Is.EqualTo(TestUploadFileContents)); - } - - [Test] - public void Can_RestClient_POST_upload_file_to_save_new_file_and_create_new_Directory() - { - var restClient = (IRestClient)CreateAsyncRestClient(); - var fileToUpload = new FileInfo(FilesRootDir + "TESTUPLOAD.txt"); - restClient.PostFile("files/UploadedFiles/", + var response = restClient.PostFile("files/UploadedFiles/", fileToUpload, MimeTypes.GetMimeType(fileToUpload.Name)); Assert.That(Directory.Exists(FilesRootDir + "UploadedFiles")); @@ -128,11 +118,11 @@ public void Can_RestClient_POST_upload_file_to_save_new_file_and_create_new_Dire } [Test] - public async Task Can_PutAsync_to_replace_text_content_of_an_existing_file() + public void Can_Put_to_replace_text_content_of_an_existing_file() { - var restClient = CreateAsyncRestClient(); + var restClient = CreateRestClient(); - var response = await restClient.PutAsync("files/README.txt", + var response = restClient.Put(WebServiceHostUrl + "files/README.txt", new Files { TextContents = ReplacedFileContents }); Assert.That(File.ReadAllText(FilesRootDir + "README.txt"), @@ -140,47 +130,49 @@ public async Task Can_PutAsync_to_replace_text_content_of_an_existing_file() } [Test] - public async Task Can_DeleteAsync_to_replace_text_content_of_an_existing_file() + public void Can_Delete_to_replace_text_content_of_an_existing_file() { - var restClient = CreateAsyncRestClient(); + var restClient = CreateRestClient(); - var response = await restClient.DeleteAsync("files/README.txt"); + var response = restClient.Delete("files/README.txt"); Assert.That(!File.Exists(FilesRootDir + "README.txt")); } /* - * Error Handling Tests - */ + * Error Handling Tests + */ [Test] - public async Task GET_a_file_that_doesnt_exist_throws_a_404_FileNotFoundException() + public void GET_a_file_that_doesnt_exist_throws_a_404_FileNotFoundException() { - var restClient = CreateAsyncRestClient(); + var restClient = CreateRestClient(); try { - await restClient.GetAsync("files/UnknownFolder"); + var response = restClient.Get(WebServiceHostUrl + "files/UnknownFolder"); + + Assert.Fail("Should fail with 404 FileNotFoundException"); } catch (WebServiceException webEx) { - var response = (FilesResponse)webEx.ResponseDto; Assert.That(webEx.StatusCode, Is.EqualTo(404)); + var response = (FilesResponse)webEx.ResponseDto; Assert.That(response.ResponseStatus.ErrorCode, Is.EqualTo(typeof(FileNotFoundException).Name)); Assert.That(response.ResponseStatus.Message, Is.EqualTo("Could not find: UnknownFolder")); } } [Test] - public async Task POST_to_an_existing_file_throws_a_500_NotSupportedException() + public void POST_to_an_existing_file_throws_a_500_NotSupportedException() { - var restClient = (IRestClient)CreateAsyncRestClient(); + var restClient = CreateRestClient(); var fileToUpload = new FileInfo(FilesRootDir + "TESTUPLOAD.txt"); try { - var response = restClient.PostFile("files/README.txt", + var response = restClient.PostFile(WebServiceHostUrl + "files/README.txt", fileToUpload, MimeTypes.GetMimeType(fileToUpload.Name)); Assert.Fail("Should fail with NotSupportedException"); @@ -196,38 +188,41 @@ public async Task POST_to_an_existing_file_throws_a_500_NotSupportedException() } [Test] - public async Task PUT_to_replace_a_non_existing_file_throws_404() + public void PUT_to_replace_a_non_existing_file_throws_404() { - var restClient = CreateAsyncRestClient(); + var restClient = CreateRestClient(); try { - await restClient.PutAsync("files/non-existing-file.txt", + var response = restClient.Put(WebServiceHostUrl + "files/non-existing-file.txt", new Files { TextContents = ReplacedFileContents }); + + Assert.Fail("Should fail with 404 FileNotFoundException"); } catch (WebServiceException webEx) { - var response = (FilesResponse)webEx.ResponseDto; Assert.That(webEx.StatusCode, Is.EqualTo(404)); + var response = (FilesResponse)webEx.ResponseDto; Assert.That(response.ResponseStatus.ErrorCode, Is.EqualTo(typeof(FileNotFoundException).Name)); Assert.That(response.ResponseStatus.Message, Is.EqualTo("Could not find: non-existing-file.txt")); } } [Test] - public async Task DELETE_a_non_existing_file_throws_404() + public void DELETE_a_non_existing_file_throws_404() { - var restClient = CreateAsyncRestClient(); + var restClient = CreateRestClient(); try { - await restClient.DeleteAsync("files/non-existing-file.txt"); + var response = restClient.Delete(WebServiceHostUrl + "files/non-existing-file.txt"); + + Assert.Fail("Should fail with 404 FileNotFoundException"); } catch (WebServiceException webEx) { - var response = (FilesResponse)webEx.ResponseDto; - Assert.That(webEx.StatusCode, Is.EqualTo(404)); + var response = (FilesResponse)webEx.ResponseDto; Assert.That(response.ResponseStatus.ErrorCode, Is.EqualTo(typeof(FileNotFoundException).Name)); Assert.That(response.ResponseStatus.Message, Is.EqualTo("Could not find: non-existing-file.txt")); } diff --git a/src/RestFiles/wwwroot/src/RestFiles.Tests/TestAppHost.cs b/src/RestFiles/wwwroot/src/RestFiles.Tests/TestAppHost.cs new file mode 100644 index 0000000..8f42443 --- /dev/null +++ b/src/RestFiles/wwwroot/src/RestFiles.Tests/TestAppHost.cs @@ -0,0 +1,30 @@ +using System.Linq; +using Funq; +using RestFiles.ServiceInterface; +using ServiceStack; + +namespace RestFiles.Tests +{ + public class TestAppHost + : AppSelfHostBase + { + public const string ListeningOn = "http://localhost:8080/"; + + public TestAppHost() + : base("HttpListener Hosts for Unit Tests", typeof(FilesService).GetAssembly()) { } + + public AppConfig Config { get; set; } + + public override void Configure(Container container) + { + this.Config = new AppConfig + { + RootDirectory = "~/App_Data/files/".MapAbsolutePath(), + TextFileExtensions = ".txt,.sln,.proj,.cs,.config,.asax".Split(',').ToList(), + }; + container.Register(this.Config); + + this.Start(ListeningOn); + } + } +} \ No newline at end of file diff --git a/src/RestFiles/wwwroot/src/Startup.cs b/src/RestFiles/wwwroot/src/Startup.cs new file mode 100644 index 0000000..7cec11a --- /dev/null +++ b/src/RestFiles/wwwroot/src/Startup.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Funq; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using RestFiles.ServiceInterface; +using ServiceStack; +using ServiceStack.Configuration; + +namespace RestFiles +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + loggerFactory.AddConsole(); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseServiceStack(new AppHost()); + } + } + + public class AppHost : AppHostBase + { + /// + /// Initializes a new instance of your ServiceStack application, with the specified name and assembly containing the services. + /// + public AppHost() : base("REST Files", typeof(FilesService).GetAssembly()) { } + + /// + /// Configure the container with the necessary routes for your ServiceStack application. + /// + /// The built-in IoC used with ServiceStack. + public override void Configure(Container container) + { + //Permit modern browsers (e.g. Firefox) to allow sending of any REST HTTP Method + Plugins.Add(new CorsFeature()); + + SetConfig(new HostConfig + { + DebugMode = true, + }); + + var config = new AppConfig + { + RootDirectory = AppSettings.GetString("RootDirectory"), + TextFileExtensions = AppSettings.GetList("TextFileExtensions").ToList(), + ExcludeDirectories = AppSettings.GetList("ExcludeDirectories").ToList(), + }; + container.Register(config); + + if (!Directory.Exists(config.RootDirectory)) + Directory.CreateDirectory(config.RootDirectory); + } + } +} diff --git a/src/RestFiles/RestFiles/App_Data/src/default.htm b/src/RestFiles/wwwroot/src/default.htm similarity index 97% rename from src/RestFiles/RestFiles/App_Data/src/default.htm rename to src/RestFiles/wwwroot/src/default.htm index 48d01fb..72dceef 100644 --- a/src/RestFiles/RestFiles/App_Data/src/default.htm +++ b/src/RestFiles/wwwroot/src/default.htm @@ -8,11 +8,11 @@ - ServiceStack Home
@@ -27,8 +27,8 @@

C# client examples

- GitHub-like browser with complete remote file management over REST in - 1 page jQuery + + GitHub-like browser with complete remote file management over REST in + 1 page jQuery + 1 page C#