0.1 #1

Merged
armin merged 30 commits from 0.1 into master 2020-07-26 00:51:55 +00:00
114 changed files with 8476 additions and 7698 deletions

View file

@ -19,6 +19,46 @@ steps:
- cd service/fling - cd service/fling
- mvn -Pprod clean deploy - mvn -Pprod clean deploy
- name: runservice
image: adoptopenjdk:11-jre-hotspot
commands:
- cd service/fling/target
- java -jar fling-0.1.0-SNAPSHOT.jar
detach: true
- name: generate-clients
image: alpine
environment:
NEXUS_USER:
from_secret: nexus_user
NEXUS_PASSWORD:
from_secret: nexus_password
commands:
- apk add --update --no-cache openjdk11 npm
- sleep 20
- npm install @openapitools/openapi-generator-cli -g
# Python client
- openapi-generator generate
-i http://runservice:8080/v3/api-docs
-g python
--additional-properties packageName=flingclient
-o flingclient.py
--enable-post-process-file
- cd flingclient.py
- cd ..
# JavaScript client
- openapi-generator generate
-i http://runservice:8080/v3/api-docs
-g javascript
--additional-properties projectName=@fling/flingclient,usePromises=true,npmRepository=https://nexus.friedl.net/repository/npm-private/
-o flingclient.js
--enable-post-process-file
- cd flingclient.js && npm install && npm run build
- echo "https://nexus.friedl.net/repository/npm-private/" >> .npmrc
- echo -n "_auth=" >> .npmrc && echo -n "$NEXUS_USER:$NEXUS_PASSWORD" | base64 >> .npmrc
- echo "email=dev@friedl.net" >> .npmrc
- npm publish
- name: build-web - name: build-web
image: node:latest image: node:latest
volumes: volumes:
@ -29,12 +69,15 @@ steps:
from_secret: nexus_user from_secret: nexus_user
NEXUS_PASSWORD: NEXUS_PASSWORD:
from_secret: nexus_password from_secret: nexus_password
VERSION: 0.1.0-snapshot
commands: commands:
- ls -al - ls -al
- cd web/fling - cd web/fling
- npm install && npm run build - npm install && npm run build
- tar czf fling-web-latest.tar.gz build/ - tar czf fling-web-$VERSION.tar.gz build/
- curl --user "$NEXUS_USER:$NEXUS_PASSWORD" --upload-file ./fling-web-latest.tar.gz https://nexus.friedl.net/repository/build-artifacts/fling-web-latest.tar.gz - curl --user "$NEXUS_USER:$NEXUS_PASSWORD"
--upload-file ./fling-web-$VERSION.tar.gz
https://nexus.friedl.net/repository/build-artifacts/fling-web-$VERSION.tar.gz
- name: publish - name: publish
image: plugins/docker image: plugins/docker
@ -46,10 +89,9 @@ steps:
dockerfile: container/Dockerfile dockerfile: container/Dockerfile
context: ./container context: ./container
repo: arminfriedl/fling repo: arminfriedl/fling
tags: latest tags: 0.1.0-snapshot
when: build_args:
branch: - VERSION=0.1.0-snapshot
- master
volumes: volumes:
- name: m2-cache - name: m2-cache

4
.gitignore vendored
View file

@ -243,7 +243,6 @@ modules.xml
*.war *.war
*.nar *.nar
*.ear *.ear
*.zip
*.tar.gz *.tar.gz
*.rar *.rar
@ -321,6 +320,7 @@ pids
*.pid *.pid
*.seed *.seed
*.pid.lock *.pid.lock
.attach_pid*
# Directory for instrumented libs generated by jscoverage/JSCover # Directory for instrumented libs generated by jscoverage/JSCover
lib-cov lib-cov
@ -500,4 +500,4 @@ gradle-app.setting
### Gradle Patch ### ### Gradle Patch ###
**/build/ **/build/
# End of https://www.gitignore.io/api/git,vim,web,node,java,react,linux,macos,emacs,maven,gradle,windows,eclipse,intellij+all,visualstudiocode # End of https://www.gitignore.io/api/git,vim,web,node,java,react,linux,macos,emacs,maven,gradle,windows,eclipse,intellij+all,visualstudiocode

9
LICENSE Normal file
View file

@ -0,0 +1,9 @@
The MIT License (MIT)
Copyright © 2020 Armin Friedl
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

1
VERSION Normal file
View file

@ -0,0 +1 @@
0.1.0-snapshot

View file

@ -1,10 +1,12 @@
FROM alpine:latest FROM alpine:latest
ARG VERSION
RUN apk add --update --no-cache nginx openjdk11-jre && \ RUN apk add --update --no-cache nginx openjdk11-jre && \
mkdir -p /var/fling/files && \ mkdir -p /var/fling/files && \
mkdir -p /tmp/fling && \ mkdir -p /tmp/fling && \
wget -O /tmp/fling/service.jar "https://nexus.friedl.net/service/rest/v1/search/assets/download?sort=version&maven.groupId=net.friedl&maven.artifactId=fling&maven.baseVersion=*SNAPSHOT&maven.extension=jar" && \ wget -O /tmp/fling/service.jar "https://nexus.friedl.net/service/rest/v1/search/assets/download?sort=version&maven.groupId=net.friedl&maven.artifactId=fling&maven.baseVersion=$(echo -n $VERSION | tr [:lower:] [:upper:])&maven.extension=jar" && \
wget -O /tmp/fling/web.tar.gz "https://nexus.friedl.net/repository/build-artifacts/fling-web-latest.tar.gz" && \ wget -O /tmp/fling/web.tar.gz "https://nexus.friedl.net/repository/build-artifacts/fling-web-$(echo -n $VERSION | tr [:upper:] [:lower:]).tar.gz" && \
tar xzf /tmp/fling/web.tar.gz -C /tmp/fling && \ tar xzf /tmp/fling/web.tar.gz -C /tmp/fling && \
ls -al /tmp/fling && \ ls -al /tmp/fling && \
mkdir -p /var/www/fling && mv /tmp/fling/build/* /var/www/fling && \ mkdir -p /var/www/fling && mv /tmp/fling/build/* /var/www/fling && \

View file

@ -27,6 +27,40 @@ server {
client_max_body_size 5G; client_max_body_size 5G;
} }
# handle openapi requests by openapi servlet
location /v3/api-docs {
proxy_pass http://localhost:8080;
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
# Required for web sockets to function
proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# handle openapi requests by openapi servlet
location /swagger-ui.html {
proxy_pass http://localhost:8080;
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
# Required for web sockets to function
proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# always respond with index.html for unknown paths # always respond with index.html for unknown paths
# (routing is client side) # (routing is client side)
location / { location / {

32
examples/python/fling.py Normal file
View file

@ -0,0 +1,32 @@
import flingclient as fc
from flingclient.rest import ApiException
from datetime import datetime
# Per default the dockerized fling service runs on localhost:3000 In case you
# run your own instance, change the base url
configuration = fc.Configuration(host="http://localhost:3000")
# Every call, with the exception of `/api/auth`, is has to be authorized by a
# bearer token. Get a token by authenticating as admin and set it into the
# configuration. All subsequent calls will send this token in the header as
# `Authorization: Bearer <token> header`
def authenticate(admin_user, admin_password):
with fc.ApiClient(configuration) as api_client:
auth_client = fc.AuthApi(api_client)
admin_auth = fc.AdminAuth(admin_user, admin_password)
configuration.access_token = auth_client.authenticate_owner(admin_auth=admin_auth)
admin_user = input("Username: ")
admin_password = input("Password: ")
authenticate(admin_user, admin_password)
with fc.ApiClient(configuration) as api_client:
# Create a new fling
fling_client = fc.FlingApi(api_client)
fling = fc.Fling(name="A Fling from Python", auth_code="secret",
direct_download=False, allow_upload=True,
expiration_time=datetime(2099, 12, 12))
fling = fling_client.post_fling()
print(f"Created a new fling: {fling}")
#

98
examples/querysheet.http Normal file
View file

@ -0,0 +1,98 @@
######################################
# Fling Querysheet for restclient.el #
######################################
# Authenticate as user
POST http://localhost:8080/api/auth/user
Content-Type: application/json
{"shareId": "shareId", "authCode":"secret"}
-> jq-set-var :token .
# :token = Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTQ0NjEzNzMsImV4cCI6MTU5NDY0MTM3Mywic3ViIjoiYWRtaW4ifQ.yu6sF1aE6sW4Jx1hBMj6iUsy8xfiaRGlIFVnHK4YkU8
# Authenticate as admin
POST http://localhost:8080/api/auth/admin
Content-Type: application/json
{"adminName": "admin", "adminPassword":"123"}
-> run-hook (restclient-set-var ":token" (buffer-substring-no-properties 1 (line-end-position)))
:token = Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTU2NzUxMjAsImV4cCI6MTU5NTg1NTEyMCwic3ViIjoiYWRtaW4ifQ.WzrGTTZTYHYOw8SskHQ2_sob2tzLIF6q8y8_2oyuafs
# Get all flings
GET http://localhost:8080/api/fling
Content-Type: application/json
:token
:flingid = 9f7353a3-efaa-41af-9f93-61e02dc5e440
# Put a fling
PUT http://localhost:8080/api/fling/:flingid
Content-Type: application/json
:token
{
"id": "9f7353a3-efaa-41af-9f93-61e02dc5e440",
"name": "Shared Fling from querysheetsdfasfd",
"creationTime": 1595253659362,
"shareId": "WWgTlNZJPZDQ6oowUYfxcQqq",
"directDownload": false,
"allowUpload": false,
"shared": true,
"expirationClicks": 12
}
# Add a new fling
POST http://localhost:8080/api/fling
Content-Type: application/json
:token
{"name": "Shared Fling from querysheet", "expirationClicks": 12, "shared": true}
# Add a new fling
POST http://localhost:8080/api/fling
Content-Type: application/json
:token
{"name": "Unshared Fling from querysheet", "expirationClicks": 12, "shared": false}
# Add a new fling
POST http://localhost:8080/api/fling
Content-Type: application/json
:token
{"name": "Fling from querysheet with Auth", "expirationClicks": 12, "shared": true, "authCode": "abc"}
# Add a new fling
POST http://localhost:8080/api/fling
Content-Type: application/json
:token
{"name": "Fling from querysheet with Auth and very long name", "expirationClicks": 12, "shared": true, "authCode": "abc"}
# GET derived auth token
GET http://localhost:8080/api/auth/derive
Content-Type: application/json
:token
:derivedToken = 56c4ff2e-7da7-4582-bd2c-9a81d9a13abb
#
:flingId = 9f7353a3-efaa-41af-9f93-61e02dc5e440
# Get one fling
GET http://localhost:8080/api/fling/:flingId
:token
# Get all artifacts
GET http://localhost:8080/api/fling/:flingId/artifacts
Content-Type: application/json
:token
:artifactId = 01ba7fb9-9f2e-4809-9b2b-cbce12a92621
# Get artifact data by derived token
GET http://localhost:8080/api/artifacts/:artifactId/data?derivedtoken=:derivedToken
Content-Type: application/json
#
GET https://httpbin.org/json
-> jq-set-var :my-var .slideshow.slides[0].title
#
GET http://httpbin.org/ip
-> run-hook (restclient-set-var ":my-ip" (cdr (assq 'origin (json-read))))

View file

@ -1,380 +1,380 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<profiles version="19"> <profiles version="19">
<profile kind="CodeFormatterProfile" name="GoogleStyle" version="19"> <profile kind="CodeFormatterProfile" name="FlingStyle" version="19">
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment" value="common_lines"/> <setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries" value="true"/> <setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_logical_operator" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_logical_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation" value="common_lines"/> <setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" value="1"/> <setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_record_declaration" value="common_lines"/> <setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_record_declaration" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement" value="common_lines"/> <setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="true"/> <setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.indentation.size" value="4"/> <setting id="org.eclipse.jdt.core.formatter.indentation.size" value="4"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration" value="common_lines"/> <setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_default" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_default" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.align_with_spaces" value="false"/> <setting id="org.eclipse.jdt.core.formatter.align_with_spaces" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.disabling_tag" value="@formatter:off"/> <setting id="org.eclipse.jdt.core.formatter.disabling_tag" value="@formatter:off"/>
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="2"/> <setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="2"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_before_code_block" value="0"/> <setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_before_code_block" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_switch_case_expressions" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_switch_case_expressions" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_enum_constants" value="0"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_enum_constants" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="0"/> <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_method_body" value="0"/> <setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_method_body" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/> <setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement" value="common_lines"/> <setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="true"/> <setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch" value="true"/> <setting id="org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.enabling_tag" value="@formatter:on"/> <setting id="org.eclipse.jdt.core.formatter.enabling_tag" value="@formatter:on"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position" value="true"/> <setting id="org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_record_components" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_record_components" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_case" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_case" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_multiplicative_operator" value="true"/> <setting id="org.eclipse.jdt.core.formatter.wrap_before_multiplicative_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="false"/> <setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations" value="2"/> <setting id="org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations" value="2"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references" value="0"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_logical_operator" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_logical_operator" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line" value="one_line_never"/> <setting id="org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_record_declaration" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_record_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_multiplicative_operator" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_multiplicative_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column" value="false"/> <setting id="org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_block" value="true"/> <setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_block" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/> <setting id="org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped" value="false"/> <setting id="org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="100"/> <setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="100"/>
<setting id="org.eclipse.jdt.core.formatter.use_on_off_tags" value="true"/> <setting id="org.eclipse.jdt.core.formatter.use_on_off_tags" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.keep_method_body_on_one_line" value="one_line_if_empty"/> <setting id="org.eclipse.jdt.core.formatter.keep_method_body_on_one_line" value="one_line_if_empty"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line" value="one_line_never"/> <setting id="org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments" value="false"/> <setting id="org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/> <setting id="org.eclipse.jdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_abstract_method" value="1"/> <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_abstract_method" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line" value="one_line_if_empty"/> <setting id="org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line" value="one_line_if_empty"/>
<setting id="org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns" value="false"/> <setting id="org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line" value="one_line_never"/> <setting id="org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/> <setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="false"/> <setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause" value="common_lines"/> <setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_additive_operator" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_additive_operator" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_constructor" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_constructor" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_relational_operator" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_relational_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiplicative_operator" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_multiplicative_operator" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line" value="one_line_if_empty"/> <setting id="org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line" value="one_line_if_empty"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_switch_case_expressions" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_switch_case_expressions" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_shift_operator" value="true"/> <setting id="org.eclipse.jdt.core.formatter.wrap_before_shift_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header" value="true"/> <setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_record_declaration_on_one_line" value="one_line_never"/> <setting id="org.eclipse.jdt.core.formatter.keep_record_declaration_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="end_of_line"/> <setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/> <setting id="org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_lambda_body" value="end_of_line"/> <setting id="org.eclipse.jdt.core.formatter.brace_position_for_lambda_body" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_code_block" value="0"/> <setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_code_block" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.compact_else_if" value="true"/> <setting id="org.eclipse.jdt.core.formatter.compact_else_if" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_bitwise_operator" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_bitwise_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line" value="false"/> <setting id="org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_type_parameters" value="0"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_type_parameters" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_loops" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_loops" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/> <setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line" value="false"/> <setting id="org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="false"/> <setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_relational_operator" value="0"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_relational_operator" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column" value="true"/> <setting id="org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="3"/> <setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="3"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation" value="common_lines"/> <setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_additive_operator" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_additive_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_string_concatenation" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_string_concatenation" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_line_comments" value="true"/> <setting id="org.eclipse.jdt.core.formatter.comment.format_line_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_record_declaration" value="end_of_line"/> <setting id="org.eclipse.jdt.core.formatter.brace_position_for_record_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.text_block_indentation" value="0"/> <setting id="org.eclipse.jdt.core.formatter.text_block_indentation" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.align_type_members_on_columns" value="false"/> <setting id="org.eclipse.jdt.core.formatter.align_type_members_on_columns" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_assignment" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_assignment" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_module_statements" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_module_statements" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/> <setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_after_code_block" value="0"/> <setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_after_code_block" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions" value="false"/> <setting id="org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line" value="one_line_never"/> <setting id="org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="0"/> <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="80"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="80"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line" value="false"/> <setting id="org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns" value="false"/> <setting id="org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/> <setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/> <setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_default" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_default" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_between_different_tags" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_between_different_tags" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression_chain" value="0"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression_chain" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="true"/> <setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_additive_operator" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_additive_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_method_declaration" value="0"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_method_declaration" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.join_wrapped_lines" value="false"/> <setting id="org.eclipse.jdt.core.formatter.join_wrapped_lines" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_conditional_operator" value="true"/> <setting id="org.eclipse.jdt.core.formatter.wrap_before_conditional_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/> <setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_shift_operator" value="0"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_shift_operator" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines" value="2147483647"/> <setting id="org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines" value="2147483647"/>
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries" value="true"/> <setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration" value="end_of_line"/> <setting id="org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_resources_in_try" value="80"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_resources_in_try" value="80"/>
<setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/> <setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause" value="common_lines"/> <setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column" value="false"/> <setting id="org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.keep_code_block_on_one_line" value="one_line_never"/> <setting id="org.eclipse.jdt.core.formatter.keep_code_block_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_record_components" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_record_components" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="2"/> <setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="2"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_bitwise_operator" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_bitwise_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_source_code" value="true"/> <setting id="org.eclipse.jdt.core.formatter.comment.format_source_code" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="0"/> <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/> <setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/> <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_record_declaration" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_record_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_assignment_operator" value="false"/> <setting id="org.eclipse.jdt.core.formatter.wrap_before_assignment_operator" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_not_operator" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_not_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_switch" value="end_of_line"/> <setting id="org.eclipse.jdt.core.formatter.brace_position_for_switch" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_html" value="true"/> <setting id="org.eclipse.jdt.core.formatter.comment.format_html" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration" value="common_lines"/> <setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_if" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_if" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line" value="one_line_never"/> <setting id="org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.indent_empty_lines" value="false"/> <setting id="org.eclipse.jdt.core.formatter.indent_empty_lines" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_type_arguments" value="0"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_type_arguments" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_record_constructor_on_one_line" value="one_line_if_empty"/> <setting id="org.eclipse.jdt.core.formatter.keep_record_constructor_on_one_line" value="one_line_if_empty"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_declaration" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/> <setting id="org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="true"/> <setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/> <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_label" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_label" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/> <setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_case" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_case" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_member_type" value="0"/> <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_member_type" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_logical_operator" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_logical_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_record_header" value="true"/> <setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_record_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_record_declaration" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_record_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases" value="true"/> <setting id="org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_bitwise_operator" value="true"/> <setting id="org.eclipse.jdt.core.formatter.wrap_before_bitwise_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_relational_operator" value="true"/> <setting id="org.eclipse.jdt.core.formatter.wrap_before_relational_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_block_comments" value="true"/> <setting id="org.eclipse.jdt.core.formatter.comment.format_block_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.indent_tag_description" value="false"/> <setting id="org.eclipse.jdt.core.formatter.comment.indent_tag_description" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="false"/> <setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_record_constructor" value="end_of_line"/> <setting id="org.eclipse.jdt.core.formatter.brace_position_for_record_constructor" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_string_concatenation" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_string_concatenation" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_last_class_body_declaration" value="0"/> <setting id="org.eclipse.jdt.core.formatter.blank_lines_after_last_class_body_declaration" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/> <setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line" value="false"/> <setting id="org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/> <setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_logical_operator" value="true"/> <setting id="org.eclipse.jdt.core.formatter.wrap_before_logical_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_shift_operator" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_shift_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_statement_group_in_switch" value="0"/> <setting id="org.eclipse.jdt.core.formatter.blank_lines_between_statement_group_in_switch" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration" value="common_lines"/> <setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_shift_operator" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_shift_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line" value="false"/> <setting id="org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line" value="one_line_never"/> <setting id="org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_record_components" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_record_components" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested" value="true"/> <setting id="org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/> <setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/> <setting id="org.eclipse.jdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_multiplicative_operator" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_multiplicative_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="0"/> <setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header" value="0"/> <setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_additive_operator" value="true"/> <setting id="org.eclipse.jdt.core.formatter.wrap_before_additive_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line" value="false"/> <setting id="org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header" value="true"/> <setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_string_concatenation" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_string_concatenation" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.join_lines_in_comments" value="true"/> <setting id="org.eclipse.jdt.core.formatter.join_lines_in_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" value="false"/> <setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_code_block" value="0"/> <setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_code_block" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_record_declaration" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_record_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="space"/> <setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="space"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_relational_operator" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_relational_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_string_concatenation" value="true"/> <setting id="org.eclipse.jdt.core.formatter.wrap_before_string_concatenation" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_import_groups" value="0"/> <setting id="org.eclipse.jdt.core.formatter.blank_lines_between_import_groups" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="100"/> <setting id="org.eclipse.jdt.core.formatter.lineSplit" value="100"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/> <setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
</profile> </profile>
</profiles> </profiles>

22
scripts/release.sh Executable file
View file

@ -0,0 +1,22 @@
#!/bin/sh
SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Go to project root
cd ${SCRIPT_DIR}
cd ..
echo "Working directory: $(pwd)"
# Determine versions
CUR_VERSION=$(cat VERSION)
NEW_VERSION=${1}
echo "Replacing ${CUR_VERSION} with ${NEW_VERSION}"
# Replace all versions
sed -i "s/${CUR_VERSION^^}/${NEW_VERSION^^}/g" service/fling/pom.xml
sed -i "s/${CUR_VERSION,,}/${NEW_VERSION,,}/g" service/fling/src/main/resources/*.yml
sed -i "s/${CUR_VERSION,,}/${NEW_VERSION,,}/g" .drone.yml
sed -i "s/${CUR_VERSION^^}/${NEW_VERSION^^}/g" .drone.yml
sed -i "s/${CUR_VERSION,,}/${NEW_VERSION,,}/g" web/fling/package.json
sed -i "s/${CUR_VERSION}/${NEW_VERSION}/g" VERSION

View file

@ -1,206 +1,252 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version> <version>2.2.6.RELEASE</version>
<relativePath /> <!-- lookup parent from repository --> <relativePath /> <!-- lookup parent from repository -->
</parent> </parent>
<groupId>net.friedl</groupId> <groupId>net.friedl</groupId>
<artifactId>fling</artifactId> <artifactId>fling</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.1.0-SNAPSHOT</version>
<name>fling</name> <name>fling</name>
<description>Simple artifact sharing</description> <description>Simple artifact sharing</description>
<properties> <properties>
<java.version>11</java.version> <java.version>11</java.version>
<mapstruct.version>1.3.1.Final</mapstruct.version> <mapstruct.version>1.3.1.Final</mapstruct.version>
<bouncycastle.version>1.64</bouncycastle.version> <bouncycastle.version>1.64</bouncycastle.version>
<jwt.version>0.11.1</jwt.version> <jwt.version>0.11.1</jwt.version>
<spring.version>${project.parent.version}</spring.version> <spring.version>${project.parent.version}</spring.version>
</properties> <fling.api.version>${project.version}</fling.api.version>
</properties>
<dependencies> <dependencies>
<!-- Spring boot starter --> <!-- Spring boot starter -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId> <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId> <artifactId>spring-boot-starter-security</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId> <artifactId>spring-boot-starter-actuator</artifactId>
</dependency> </dependency>
<!-- Json Web Token --> <!-- Json Web Token -->
<dependency> <dependency>
<groupId>io.jsonwebtoken</groupId> <groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId> <artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version> <version>${jwt.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.jsonwebtoken</groupId> <groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId> <artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version> <version>${jwt.version}</version>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.jsonwebtoken</groupId> <groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version> <version>${jwt.version}</version>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<!-- Test dependencies --> <!-- Test dependencies -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
<exclusions> <exclusions>
<exclusion> <exclusion>
<groupId>org.junit.vintage</groupId> <groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId> <artifactId>junit-vintage-engine</artifactId>
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<!-- Annotation processors --> <!-- Annotation processors -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId> <artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.mapstruct</groupId> <groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId> <artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version> <version>${mapstruct.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build> <!-- OpenAPI Generator -->
<resources> <dependency>
<resource> <groupId>org.springdoc</groupId>
<!-- Replace @spring.profiles.active@ in application.yml by setting in <artifactId>springdoc-openapi-ui</artifactId>
maven profile See also: profiles section --> <version>1.2.32</version>
<directory>src/main/resources</directory> </dependency>
<filtering>true</filtering> </dependencies>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>${spring.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<executions>
<execution>
<id>default-deploy</id>
<phase>deploy</phase>
<goals>
<goal>deploy</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<distributionManagement> <build>
<snapshotRepository> <resources>
<id>nexus-snapshots</id> <resource>
<url>https://nexus.friedl.net/repository/maven-snapshots/</url> <!--
</snapshotRepository> Replace @...@ placeholder in resources.
</distributionManagement> Replaces:
- application.yml | @spring.profiles.active@ | see also: profiles section
- application.yml | @fling.api.version@ | see also: properties
-->
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>pre-integration-test</id>
<goals>
<goal>start</goal>
</goals>
</execution>
<execution>
<id>post-integration-test</id>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
<profiles> <!-- Generators -->
<profile> <plugin>
<!-- Profile for local development --> <groupId>org.apache.maven.plugins</groupId>
<id>local</id> <artifactId>maven-compiler-plugin</artifactId>
<properties> <configuration>
<spring.profiles.active>local</spring.profiles.active> <source>${java.version}</source>
<!-- automatically run annotation processors within the incremental compilation --> <target>${java.version}</target>
<m2e.apt.activation>jdt_apt</m2e.apt.activation> <annotationProcessorPaths>
</properties> <path>
<dependencies> <groupId>org.mapstruct</groupId>
<dependency> <artifactId>mapstruct-processor</artifactId>
<groupId>org.springframework.boot</groupId> <version>${mapstruct.version}</version>
<artifactId>spring-boot-devtools</artifactId> </path>
<scope>runtime</scope> <path>
<optional>true</optional> <groupId>org.projectlombok</groupId>
</dependency> <artifactId>lombok</artifactId>
<dependency> <version>${lombok.version}</version>
<groupId>org.springframework.boot</groupId> </path>
<artifactId>spring-boot-configuration-processor</artifactId> <path>
<optional>true</optional> <groupId>org.springframework.boot</groupId>
</dependency> <artifactId>spring-boot-configuration-processor</artifactId>
<dependency> <version>${spring.version}</version>
<groupId>com.h2database</groupId> </path>
<artifactId>h2</artifactId> </annotationProcessorPaths>
<scope>runtime</scope> </configuration>
</dependency> </plugin>
</dependencies>
</profile> <!-- OpenApi -->
<profile> <plugin>
<!-- Profile for prod deployment --> <groupId>org.springdoc</groupId>
<id>prod</id> <artifactId>springdoc-openapi-maven-plugin</artifactId>
<properties> <version>1.0</version>
<spring.profiles.active>prod</spring.profiles.active> <executions>
</properties> <execution>
<dependencies> <phase>integration-test</phase>
<dependency> <goals>
<groupId>com.h2database</groupId> <goal>generate</goal>
<artifactId>h2</artifactId> </goals>
<scope>runtime</scope> </execution>
</dependency> </executions>
</dependencies> <configuration>
</profile> <outputFileName>flingapi.json</outputFileName>
</profiles> <outputDir>${project.build.directory}/openapi-spec</outputDir>
<!-- Attach api doc to deployed artifact -->
<attachArtifact>true</attachArtifact>
</configuration>
</plugin>
<!-- Deployment -->
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<executions>
<execution>
<id>default-deploy</id>
<phase>deploy</phase>
<goals>
<goal>deploy</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<distributionManagement>
<snapshotRepository>
<id>nexus-snapshots</id>
<url>https://nexus.friedl.net/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
<profiles>
<profile>
<!-- Profile for local development -->
<id>local</id>
<properties>
<spring.profiles.active>local</spring.profiles.active>
<!-- automatically run annotation processors within the incremental
compilation -->
<m2e.apt.activation>jdt_apt</m2e.apt.activation>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</profile>
<profile>
<!-- Profile for prod deployment -->
<id>prod</id>
<properties>
<spring.profiles.active>prod</spring.profiles.active>
</properties>
<dependencies>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</profile>
</profiles>
</project> </project>

View file

@ -1,30 +1,41 @@
package net.friedl.fling; package net.friedl.fling;
import java.security.MessageDigest; import java.nio.file.Path;
import java.security.NoSuchAlgorithmException;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import net.friedl.fling.model.json.PathDeserializer;
import net.friedl.fling.model.json.PathSerializer;
@Configuration @Configuration
public class FlingConfiguration { public class FlingConfiguration {
@Bean @Bean
public MessageDigest keyHashDigest() throws NoSuchAlgorithmException { public PasswordEncoder passwordEncoder() {
return MessageDigest.getInstance("SHA-512"); return new BCryptPasswordEncoder();
} }
@Bean @Bean
public ObjectMapper objectMapper() { public ObjectMapper objectMapper() {
SimpleModule simpleModule = new SimpleModule();
simpleModule
.addDeserializer(Path.class, new PathDeserializer())
.addSerializer(Path.class, new PathSerializer());
ObjectMapper objectMapper = new ObjectMapper() ObjectMapper objectMapper = new ObjectMapper()
.setSerializationInclusion(Include.NON_ABSENT) .setSerializationInclusion(Include.NON_ABSENT)
.registerModule(new JavaTimeModule()) .registerModule(new JavaTimeModule())
// Handle instant as milliseconds // Handle instant as milliseconds
.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false) .configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false); .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
.registerModule(simpleModule);
return objectMapper; return objectMapper;
} }

View file

@ -1,8 +1,7 @@
package net.friedl.fling.security; package net.friedl.fling;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.Key; import java.security.Key;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -11,30 +10,22 @@ import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.Keys;
import lombok.Data; import lombok.Data;
@Configuration
@Data @Data
@Configuration
@ConfigurationProperties("fling.security") @ConfigurationProperties("fling.security")
public class FlingSecurityConfiguration { public class FlingSecurityConfiguration {
private List<String> allowedOrigins;
private String adminUser;
private String adminPassword;
private String signingKey; private String signingKey;
private Long jwtExpiration; @Bean
public JwtParser jwtParser() {
return Jwts.parserBuilder()
.setSigningKey(jwtSigningKey())
.build();
}
@Bean @Bean
public Key jwtSigningKey() { public Key jwtSigningKey() {
byte[] key = signingKey.getBytes(StandardCharsets.UTF_8); byte[] key = signingKey.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(key); return Keys.hmacShaKeyFor(key);
} }
@Bean
public JwtParser jwtParser(Key jwtSignigKey) {
return Jwts.parserBuilder()
.setSigningKey(jwtSignigKey)
.build();
}
} }

View file

@ -1,6 +1,7 @@
package net.friedl.fling.controller; package net.friedl.fling.controller;
import java.util.List; import java.io.IOException;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.InputStreamResource;
@ -8,75 +9,70 @@ import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import net.friedl.fling.model.dto.ArtifactDto; import net.friedl.fling.model.dto.ArtifactDto;
import net.friedl.fling.persistence.archive.ArchiveException;
import net.friedl.fling.service.ArtifactService; import net.friedl.fling.service.ArtifactService;
import net.friedl.fling.service.archive.ArchiveService;
@RestController @RestController
@RequestMapping("/api") @RequestMapping("/api/artifacts")
@Tag(name = "artifact", description = "Operations on /api/artifacts")
@SecurityRequirement(name = "bearer")
@Validated
public class ArtifactController { public class ArtifactController {
private ArtifactService artifactService; private ArtifactService artifactService;
private ArchiveService archiveService;
@Autowired @Autowired
public ArtifactController(ArtifactService artifactService) { public ArtifactController(ArtifactService artifactService, ArchiveService archiveService) {
this.artifactService = artifactService; this.artifactService = artifactService;
this.archiveService = archiveService;
} }
@GetMapping(path = "/artifacts", params = "flingId") @ApiResponse(responseCode = "404", description = "No artifact with `id` found")
public List<ArtifactDto> getArtifacts(@RequestParam Long flingId) { @GetMapping(path = "/{id}")
return artifactService.findAllArtifacts(flingId); public ArtifactDto getArtifact(@PathVariable UUID id) {
return artifactService.getById(id);
} }
@GetMapping(path = "/artifacts", params = "artifactId") @ApiResponse(responseCode = "404", description = "No artifact with `id` found")
public ResponseEntity<ArtifactDto> getArtifact(@RequestParam Long artifactId) { @DeleteMapping(path = "/{id}")
return ResponseEntity.of(artifactService.findArtifact(artifactId)); public void deleteArtifact(@PathVariable UUID id) throws IOException {
artifactService.delete(id);
} }
@PostMapping("/artifacts/{flingId}") @RequestBody(content = @Content(schema = @Schema(type = "string", format = "binary")))
public ArtifactDto postArtifact(@PathVariable Long flingId, HttpServletRequest request) @PostMapping(path = "/{id}/data")
throws Exception { public void uploadArtifactData(@PathVariable UUID id, HttpServletRequest request)
return artifactService.storeArtifact(flingId, request.getInputStream()); throws IOException {
archiveService.storeArtifact(id, request.getInputStream());
} }
@PatchMapping(path = "/artifacts/{artifactId}", consumes = MediaType.APPLICATION_JSON_VALUE) @ApiResponse(responseCode = "200",
public ArtifactDto patchArtifact(@PathVariable Long artifactId, @RequestBody String body) { content = @Content(schema = @Schema(type = "string", format = "binary")))
return artifactService.mergeArtifact(artifactId, body); @GetMapping(path = "/{id}/data")
} public ResponseEntity<Resource> downloadArtifact(@PathVariable UUID id) throws IOException {
ArtifactDto artifactDto = artifactService.getById(id);
@DeleteMapping(path = "/artifacts/{artifactId}") InputStreamResource data = new InputStreamResource(archiveService.getArtifact(id));
public void deleteArtifact(@PathVariable Long artifactId) throws ArchiveException {
artifactService.deleteArtifact(artifactId);
}
@GetMapping(path = "/artifacts/{artifactId}/downloadid")
public String getDownloadId(@PathVariable Long artifactId) {
return artifactService.generateDownloadId(artifactId);
}
@GetMapping(path = "/artifacts/{artifactId}/{downloadId}/download")
public ResponseEntity<Resource> downloadArtifact(@PathVariable Long artifactId,
@PathVariable String downloadId)
throws ArchiveException {
var artifact = artifactService.findArtifact(artifactId).orElseThrow();
var stream = new InputStreamResource(artifactService.downloadArtifact(downloadId));
return ResponseEntity.ok() return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, .header(HttpHeaders.CONTENT_DISPOSITION,
"attachment;filename=\"" + artifact.getName() + "\"") "attachment;filename=\"" + artifactDto.getPath().getFileName() + "\"")
.contentLength(artifact.getSize())
.contentType(MediaType.APPLICATION_OCTET_STREAM) .contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(stream); .body(data);
} }
} }

View file

@ -0,0 +1,78 @@
package net.friedl.fling.controller;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import net.friedl.fling.model.dto.AdminAuthDto;
import net.friedl.fling.model.dto.UserAuthDto;
import net.friedl.fling.security.FlingWebSecurityConfigurer;
import net.friedl.fling.service.AuthenticationService;
@RestController
@RequestMapping("/api/auth")
@Tag(name = "auth", description = "Operations on /api/auth")
@Validated
public class AuthenticationController {
private AuthenticationService authenticationService;
@Autowired
public AuthenticationController(AuthenticationService authenticationService) {
this.authenticationService = authenticationService;
}
@Operation(description = "Authenticates the fling admin by username and password")
@ApiResponse(responseCode = "200",
description = "JWT Token authenticating the admin of this fling instance")
@ApiResponse(responseCode = "403",
description = "Authentication failed, username or password are wrong")
@PostMapping(path = "/admin")
public String authenticateOwner(@RequestBody AdminAuthDto adminAuthDto) {
return authenticationService.authenticate(adminAuthDto)
.orElseThrow(() -> new AccessDeniedException("Wrong username or password"));
}
@Operation(description = "Authenticates a fling user for a fling via code")
@ApiResponse(responseCode = "200",
description = "JWT Token authenticating the user for a fling")
@ApiResponse(responseCode = "403",
description = "Authentication failed, the provided code for the fling is wrong")
@ApiResponse(responseCode = "404",
description = "No fling for the given share id exists")
@PostMapping("/user")
public String authenticateUser(@RequestBody UserAuthDto userAuthDto) {
return authenticationService.authenticate(userAuthDto)
.orElseThrow(() -> new AccessDeniedException("Wrong username or password"));
}
//@formatter:off
/**
* Note that this endpoint is not protected. But the token will only get authority of the
* authenticated user.
* @see FlingWebSecurityConfigurer
* @see AuthenticationService
*/
@Operation(description = "Generate a derived token from the current authorization")
@ApiResponse(responseCode = "200", description = "Token impersonating the user")
@SecurityRequirement(name = "bearer")
@GetMapping("/derive")
public String deriveToken(
@Parameter(allowEmptyValue = true, description = "Token can only be used for authorizing one request. Defaults to true")
@RequestParam Optional<Boolean> singleUse)
{
return authenticationService.deriveToken(singleUse.orElse(true));
}
//@formatter:on
}

View file

@ -0,0 +1,45 @@
package net.friedl.fling.controller;
import java.io.IOException;
import java.io.UncheckedIOException;
import javax.persistence.EntityNotFoundException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@ControllerAdvice
public class CommonExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({EntityNotFoundException.class, IOException.class, UncheckedIOException.class})
public ResponseEntity<Object> handleNotFound(Exception ex, WebRequest request)
throws Exception {
HttpHeaders headers = new HttpHeaders();
if (ex instanceof IOException) {
log.error("IO Error", ex);
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
return handleExceptionInternal(ex, null, headers, status, request);
}
if (ex instanceof UncheckedIOException) {
log.error("IO Error", ex);
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
return handleExceptionInternal(ex, null, headers, status, request);
}
if (ex instanceof EntityNotFoundException) {
log.error("Entity not found", ex);
HttpStatus status = HttpStatus.NOT_FOUND;
return handleExceptionInternal(ex, null, headers, status, request);
}
return super.handleException(ex, request);
}
}

View file

@ -2,6 +2,9 @@ package net.friedl.fling.controller;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.UUID;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
@ -15,76 +18,99 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import net.friedl.fling.model.dto.ArtifactDto;
import net.friedl.fling.model.dto.FlingDto; import net.friedl.fling.model.dto.FlingDto;
import net.friedl.fling.persistence.archive.ArchiveException; import net.friedl.fling.service.ArtifactService;
import net.friedl.fling.service.FlingService; import net.friedl.fling.service.FlingService;
import net.friedl.fling.service.archive.ArchiveService;
@RestController @RestController
@RequestMapping("/api") @RequestMapping("/api/fling")
@Tag(name = "fling", description = "Operations on /api/fling")
@SecurityRequirement(name = "bearer")
public class FlingController { public class FlingController {
private FlingService flingService; private FlingService flingService;
private ArtifactService artifactService;
private ArchiveService archiveService;
@Autowired @Autowired
public FlingController(FlingService flingService) { public FlingController(FlingService flingService, ArtifactService artifactService,
ArchiveService archiveService) {
this.flingService = flingService; this.flingService = flingService;
this.artifactService = artifactService;
this.archiveService = archiveService;
} }
@GetMapping("/fling") @GetMapping
public List<FlingDto> getFlings() { public List<FlingDto> getFlings() {
return flingService.findAll(); return flingService.findAll();
} }
@PostMapping("/fling") @PostMapping
public Long postFling(@RequestBody FlingDto flingDto) { public FlingDto postFling(@RequestBody @Valid FlingDto flingDto) {
return flingService.createFling(flingDto); return flingService.create(flingDto);
} }
@PutMapping("/fling/{flingId}") @PutMapping("/{id}")
public void putFling(@PathVariable Long flingId, @RequestBody FlingDto flingDto) { public FlingDto putFling(@PathVariable UUID id, @RequestBody @Valid FlingDto flingDto) {
flingService.mergeFling(flingId, flingDto); return flingService.replace(id, flingDto);
} }
@GetMapping(path = "/fling", params = "flingId") @PostMapping("/{id}/artifacts")
public ResponseEntity<FlingDto> getFling(@RequestParam Long flingId) { public ArtifactDto postArtifact(@PathVariable UUID id,
return ResponseEntity.of(flingService.findFlingById(flingId)); @RequestBody @Valid ArtifactDto artifactDto) {
return artifactService.create(id, artifactDto);
} }
@GetMapping(path = "/fling", params = "shareId") @GetMapping("/{id}/artifacts")
public ResponseEntity<FlingDto> getFlingByShareId(@RequestParam String shareId) { public Set<ArtifactDto> getArtifacts(@PathVariable UUID id) {
return ResponseEntity.of(flingService.findFlingByShareId(shareId)); return flingService.getArtifacts(id);
} }
@GetMapping(path = "/fling/shareExists/{shareId}") @GetMapping(path = "/{id}")
public Boolean getShareExists(@PathVariable String shareId) { public FlingDto getFling(@PathVariable UUID id) {
return flingService.existsShareUrl(shareId); return flingService.getById(id);
} }
@DeleteMapping("/fling/{flingId}") @GetMapping(path = "/share/{shareId}")
public void deleteFling(@PathVariable Long flingId) { public FlingDto getFlingByShareId(@PathVariable String shareId) {
flingService.deleteFlingById(flingId); return flingService.getByShareId(shareId);
} }
@GetMapping(path = "/fling/{flingId}/package") @DeleteMapping("/{id}")
public String packageFling(@PathVariable Long flingId) throws IOException, ArchiveException { public void deleteFling(@PathVariable UUID id) throws IOException {
return flingService.packageFling(flingId); flingService.delete(id);
} }
@GetMapping(path = "/fling/{flingId}/download/{downloadId}") @Operation(responses = {
public ResponseEntity<Resource> downloadFling(@PathVariable Long flingId, @ApiResponse(responseCode = "200",
@PathVariable String downloadId) throws ArchiveException, IOException { content = @Content(
var fling = flingService.findFlingById(flingId).orElseThrow(); mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE,
var flingPackage = flingService.downloadFling(downloadId); schema = @Schema(type = "string", format = "binary")))
var stream = new InputStreamResource(flingPackage.getFirst()); })
@GetMapping(path = "/{id}/data")
public ResponseEntity<Resource> getFlingData(@PathVariable UUID id) throws IOException {
FlingDto flingDto = flingService.getById(id);
InputStreamResource data = new InputStreamResource(archiveService.getFling(id));
Long length = data.contentLength();
data = new InputStreamResource(archiveService.getFling(id));
return ResponseEntity.ok() return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, .header(HttpHeaders.CONTENT_DISPOSITION,
"attachment;filename=\"" + fling.getName() + ".zip" + "\"") "attachment;filename=\"" + flingDto.getName() + ".zip" + "\"")
.contentLength(flingPackage.getSecond()) .contentLength(length)
.contentType(MediaType.APPLICATION_OCTET_STREAM) .contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(stream); .body(data);
} }
} }

View file

@ -0,0 +1,71 @@
package net.friedl.fling.controller;
import java.util.Optional;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.security.SecurityScheme.Type;
import io.swagger.v3.oas.models.servers.Server;
import lombok.Data;
@Data
@Configuration
@ConfigurationProperties("fling.api")
public class OpenApiConfiguration {
private String version;
private String serverUrl;
private String serverDescription;
@Bean
public OpenAPI openApi() {
OpenAPI openApi = new OpenAPI()
.components(new Components()
.addSecuritySchemes("bearer", bearerScheme())
.addSchemas("resource", new Schema<Resource>()
.type("string").format("binary")))
.info(apiInfo());
serverItem().ifPresent(openApi::addServersItem);
return openApi;
}
public SecurityScheme bearerScheme() {
return new SecurityScheme()
.name("bearerAuth")
.type(Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT");
}
public Info apiInfo() {
return new Info()
.contact(new Contact()
.name("Armin Friedl")
.url("https://www.friedl.net")
.email("dev@friedl.net"))
.license(new License()
.name("The MIT License (MIT)"))
.title("The Fling API")
.description("Share file collections with expiration, protection and short urls")
.version(version);
}
public Optional<Server> serverItem() {
if (serverUrl == null) return Optional.empty();
return Optional.of(
new Server()
.description(serverDescription)
.url(serverUrl));
}
}

View file

@ -0,0 +1,21 @@
package net.friedl.fling.model.dto;
import javax.validation.constraints.NotNull;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
@Data
@RequiredArgsConstructor
@AllArgsConstructor
@Builder
@Schema(name = "AdminAuth")
public class AdminAuthDto {
@NotNull
private String adminName;
@NotNull
private String adminPassword;
}

View file

@ -1,23 +1,36 @@
package net.friedl.fling.model.dto; package net.friedl.fling.model.dto;
import java.nio.file.Path;
import java.time.Instant; import java.time.Instant;
import java.util.UUID;
import javax.validation.constraints.NotNull;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
@Data @Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Schema(name = "Artifact")
public class ArtifactDto { public class ArtifactDto {
private String name; @Schema(accessMode = AccessMode.READ_ONLY, type = "string")
private UUID id;
private Long id; @Schema(type = "string",
description = "Path of the artifact")
@NotNull
private Path path;
private String path; @Schema(type = "integer", format = "int64", accessMode = AccessMode.READ_ONLY,
description = "Creation time in milliseconds since the unix epoch 01.01.1970 00:00:00 UTC")
private Instant creationTime;
private String doi; @Schema(accessMode = AccessMode.READ_ONLY, type = "boolean",
description = "Whether the artifact was successfully persisted in the archive.")
private Long size; @Builder.Default
private Boolean archived = false;
private Integer version;
private Instant uploadTime;
private FlingDto fling;
} }

View file

@ -1,96 +1,55 @@
package net.friedl.fling.model.dto; package net.friedl.fling.model.dto;
import java.time.Instant; import java.time.Instant;
import java.util.HashMap; import java.util.UUID;
import java.util.Map; import javax.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema;
import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
@Data @Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "Fling")
public class FlingDto { public class FlingDto {
@Schema(accessMode = AccessMode.READ_ONLY, type = "string")
private UUID id;
@Schema(description = "Name of the fling")
@NotNull
private String name; private String name;
private Long id; @Schema(type = "integer", format = "int64", accessMode = AccessMode.READ_ONLY,
description = "Creation time in milliseconds since the unix epoch 01.01.1970 00:00:00 UTC")
private Instant creationTime; private Instant creationTime;
@JsonIgnore @Schema(description = "Share id of the fling. Used in the share link.")
private Boolean directDownload; private String shareId;
@JsonIgnore
private Boolean allowUpload;
@JsonIgnore
private Boolean shared;
@JsonIgnore
private String shareUrl;
@JsonIgnore
private Integer expirationClicks;
@JsonIgnore
private Instant expirationTime;
@Schema(description = "Authentication code for password protecting a fling.")
private String authCode; private String authCode;
@JsonProperty("sharing") @Schema(description = "Whether users should be redirected to fling download when accessing the "
private void unpackSharing(Map<String, Object> sharing) { + "fling by share id")
this.directDownload = (Boolean) sharing.getOrDefault("directDownload", false); @Builder.Default
this.allowUpload = (Boolean) sharing.getOrDefault("allowUpload", false); private Boolean directDownload = false;
this.shared = (Boolean) sharing.getOrDefault("shared", true);
this.shareUrl = (String) sharing.getOrDefault("shareUrl", null);
}
@JsonProperty("sharing") @Schema(description = "Allow uploads to the fling by users")
private Map<String, Object> packSharing() { @Builder.Default
Map<String, Object> sharing = new HashMap<>(); private Boolean allowUpload = false;
sharing.put("directDownload", this.directDownload);
sharing.put("allowUpload", this.allowUpload);
sharing.put("shared", this.shared);
sharing.put("shareUrl", this.shareUrl);
return sharing; @Schema(description = "Whether the fling is accessible by users via the share id")
} @Builder.Default
private Boolean shared = true;
@JsonProperty("expiration") @Schema(description = "How many clicks are left until the fling access by share id is disallowed")
private void unpackExpiration(Map<String, Object> expiration) { private Integer expirationClicks;
String type = (String) expiration.getOrDefault("type", null);
if (type == null)
return;
switch (type) { @Schema(type = "integer", format = "int64",
case "time": description = "Expiration time in milliseconds since the unix epoch 01.01.1970 00:00:00 UTC")
this.expirationClicks = null; private Instant expirationTime;
// json can only handle int, long must be given as string
// TODO: this back and forth conversion is a bit hack-ish
this.expirationTime =
Instant.ofEpochMilli(Long.valueOf(expiration.get("value").toString()));
break;
case "clicks":
this.expirationTime = null;
this.expirationClicks = Integer.valueOf(expiration.get("value").toString());
break;
default:
throw new IllegalArgumentException("Unexpected value '" + type + "'");
}
}
@JsonProperty("expiration")
private Map<String, Object> packExpiration() {
Map<String, Object> expiration = new HashMap<>();
if (this.expirationClicks != null) {
expiration.put("type", "clicks");
expiration.put("value", this.expirationClicks);
}
if (this.expirationTime != null) {
expiration.put("type", "time");
expiration.put("value", this.expirationTime.toEpochMilli());
}
return expiration;
}
} }

View file

@ -1,12 +0,0 @@
package net.friedl.fling.model.dto;
import lombok.Data;
@Data
public class FlingSharingDto {
private Boolean allowUpload;
private Boolean directDownload;
private String shareUrl;
}

View file

@ -0,0 +1,21 @@
package net.friedl.fling.model.dto;
import javax.validation.constraints.NotNull;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
@Data
@RequiredArgsConstructor
@AllArgsConstructor
@Builder
@Schema(name = "UserAuth")
public class UserAuthDto {
@NotNull
String shareId;
@NotNull
String authCode;
}

View file

@ -0,0 +1,34 @@
package net.friedl.fling.model.json;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
public class PathDeserializer extends StdDeserializer<Path> {
private static final long serialVersionUID = 1504807365764537418L;
public PathDeserializer() {
this(String.class);
}
protected PathDeserializer(Class<?> vc) {
super(vc);
}
@Override
public Path deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
ObjectCodec codec = p.getCodec();
JsonNode node = codec.readTree(p);
return Paths.get(node.textValue());
}
}

View file

@ -0,0 +1,27 @@
package net.friedl.fling.model.json;
import java.io.IOException;
import java.nio.file.Path;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
public class PathSerializer extends StdSerializer<Path> {
public PathSerializer() {
this(Path.class);
}
protected PathSerializer(Class<Path> t) {
super(t);
}
private static final long serialVersionUID = -1003917305429893614L;
@Override
public void serialize(Path value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
gen.writeString(value.toString());
}
}

View file

@ -1,49 +1,20 @@
package net.friedl.fling.model.mapper; package net.friedl.fling.model.mapper;
import java.lang.reflect.Field;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Set;
import java.util.Optional;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import lombok.extern.slf4j.Slf4j;
import net.friedl.fling.model.dto.ArtifactDto; import net.friedl.fling.model.dto.ArtifactDto;
import net.friedl.fling.persistence.entities.ArtifactEntity; import net.friedl.fling.persistence.entities.ArtifactEntity;
@Slf4j
@Mapper(componentModel = "spring") @Mapper(componentModel = "spring")
public abstract class ArtifactMapper { public interface ArtifactMapper {
public abstract ArtifactDto map(ArtifactEntity artifactEntity); ArtifactDto map(ArtifactEntity artifactEntity);
public abstract ArtifactEntity map(ArtifactDto artifactDto); ArtifactEntity map(ArtifactDto artifactDto);
public abstract List<ArtifactDto> map(List<ArtifactEntity> artifactEntities); List<ArtifactDto> mapEntities(List<ArtifactEntity> artifactEntities);
public Optional<ArtifactDto> map(Optional<ArtifactEntity> artifactEntity) { Set<ArtifactDto> mapEntities(Set<ArtifactEntity> artifactEntities);
return artifactEntity.map(a -> map(a));
}
public ArtifactDto merge(ArtifactDto originalArtifactDto, Map<String, Object> patch) { List<ArtifactEntity> mapDtos(List<ArtifactDto> artifactDtos);
ArtifactDto mergedArtifactDto = new ArtifactDto();
for (Field field : ArtifactDto.class.getDeclaredFields()) {
String fieldName = field.getName();
field.setAccessible(true);
try {
if (patch.containsKey(fieldName)) {
if (field.getType().equals(Long.class)) {
field.set(mergedArtifactDto, ((Number) patch.get(fieldName)).longValue());
}
field.set(mergedArtifactDto, patch.get(fieldName));
} else {
field.set(mergedArtifactDto, field.get(originalArtifactDto));
}
} catch (IllegalArgumentException | IllegalAccessException e) {
log.error("Could not merge {} [value={}] with {}", fieldName, patch.get(fieldName),
originalArtifactDto,
e);
}
}
return mergedArtifactDto;
}
} }

View file

@ -1,7 +1,6 @@
package net.friedl.fling.model.mapper; package net.friedl.fling.model.mapper;
import java.util.List; import java.util.List;
import java.util.Optional;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import net.friedl.fling.model.dto.FlingDto; import net.friedl.fling.model.dto.FlingDto;
import net.friedl.fling.persistence.entities.FlingEntity; import net.friedl.fling.persistence.entities.FlingEntity;
@ -10,11 +9,9 @@ import net.friedl.fling.persistence.entities.FlingEntity;
public interface FlingMapper { public interface FlingMapper {
FlingDto map(FlingEntity flingEntity); FlingDto map(FlingEntity flingEntity);
default Optional<FlingDto> map(Optional<FlingEntity> flingEntity) {
return flingEntity.map(f -> map(f));
}
FlingEntity map(FlingDto flingDto); FlingEntity map(FlingDto flingDto);
List<FlingDto> map(List<FlingEntity> flingEntities); List<FlingDto> mapEntities(List<FlingEntity> flingEntities);
List<FlingEntity> mapDtos(List<FlingDto> flingDtos);
} }

View file

@ -1,40 +0,0 @@
package net.friedl.fling.persistence.archive;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public interface Archive {
/**
* Retrieve an artifact from the archive
*
* @param id The unique artifact id as returned by {@link Archive#store}
* @return An {@link InputStream} for reading the artifact
*/
InputStream get(String id) throws ArchiveException;
/**
* Store an artifact
*
* @param is The artifact represented as {@link InputStream}
* @return A unique archive id for the artifact
* @throws IOException If anything goes wrong while storing the artifact in the archive
*/
String store(InputStream is) throws ArchiveException;
default String store(File file) throws ArchiveException {
try {
return store(new FileInputStream(file));
} catch (IOException ex) {
throw new ArchiveException(ex);
}
}
/**
* Delete an artifact
*
* @param id The unique artifact id as returned by {@link Archive#store}
*/
void remove(String id) throws ArchiveException;
}

View file

@ -1,73 +0,0 @@
package net.friedl.fling.persistence.archive;
public class ArchiveException extends Exception {
private static final long serialVersionUID = 6216735865308056261L;
/**
* Constructs a new exception with {@code null} as its detail message. The cause is not
* initialized, and may subsequently be initialized by a call to {@link #initCause}.
*/
public ArchiveException() {
super();
}
/**
* Constructs a new exception with the specified detail message. The cause is not initialized, and
* may subsequently be initialized by a call to {@link #initCause}.
*
* @param message the detail message. The detail message is saved for later retrieval by the
* {@link #getMessage()} method.
*/
public ArchiveException(String message) {
super(message);
}
/**
* Constructs a new exception with the specified detail message and cause.
* <p>
* Note that the detail message associated with {@code cause} is <i>not</i> automatically
* incorporated in this exception's detail message.
*
* @param message the detail message (which is saved for later retrieval by the
* {@link #getMessage()} method).
* @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
* (A {@code null} value is permitted, and indicates that the cause is nonexistent or
* unknown.)
* @since 1.4
*/
public ArchiveException(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructs a new exception with the specified cause and a detail message of
* {@code (cause==null ? null : cause.toString())} (which typically contains the class and detail
* message of {@code cause}). This constructor is useful for exceptions that are little more than
* wrappers for other throwables (for example,
* {@link java.security.PrivilegedActionArchiveException}).
*
* @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
* (A {@code null} value is permitted, and indicates that the cause is nonexistent or
* unknown.)
* @since 1.4
*/
public ArchiveException(Throwable cause) {
super(cause);
}
/**
* Constructs a new exception with the specified detail message, cause, suppression enabled or
* disabled, and writable stack trace enabled or disabled.
*
* @param message the detail message.
* @param cause the cause. (A {@code null} value is permitted, and indicates that the cause is
* nonexistent or unknown.)
* @param enableSuppression whether or not suppression is enabled or disabled
* @param writableStackTrace whether or not the stack trace should be writable
* @since 1.7
*/
protected ArchiveException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View file

@ -1,80 +0,0 @@
package net.friedl.fling.persistence.archive.impl;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import net.friedl.fling.persistence.archive.Archive;
import net.friedl.fling.persistence.archive.ArchiveException;
@Component("fileSystemArchive")
public class FileSystemArchive implements Archive {
private MessageDigest fileStoreDigest;
private FileSystemArchiveConfiguration configuration;
@Autowired
public FileSystemArchive(MessageDigest fileStoreDigest,
FileSystemArchiveConfiguration configuration) {
this.fileStoreDigest = fileStoreDigest;
this.configuration = configuration;
}
@Override
public InputStream get(String id) throws ArchiveException {
try {
var path = Paths.get(configuration.getDirectory(), id);
FileInputStream fis = new FileInputStream(path.toFile());
return fis;
} catch (FileNotFoundException ex) {
throw new ArchiveException(ex);
}
}
@Override
public String store(InputStream is) throws ArchiveException {
try {
byte[] fileBytes = is.readAllBytes();
is.close();
String fileStoreId = hexEncode(fileStoreDigest.digest(fileBytes));
FileChannel fc = FileChannel.open(Paths.get(configuration.getDirectory(), fileStoreId),
StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE,
StandardOpenOption.CREATE);
fc.write(ByteBuffer.wrap(fileBytes));
fc.close();
return fileStoreId;
} catch (IOException ex) {
throw new ArchiveException(ex);
}
}
@Override
public void remove(String id) throws ArchiveException {
var path = Paths.get(configuration.getDirectory(), id);
try {
Files.deleteIfExists(path);
} catch (IOException e) {
throw new ArchiveException("Could not delete file at " + path.toString(), e);
}
}
private String hexEncode(byte[] fileStoreId) {
StringBuilder sb = new StringBuilder(fileStoreId.length * 2);
for (byte b : fileStoreId)
sb.append(String.format("%02x", b));
return sb.toString();
}
}

View file

@ -1,41 +0,0 @@
package net.friedl.fling.persistence.archive.impl;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.annotation.PostConstruct;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Configuration
@ConfigurationProperties("fling.archive.fileystem")
@ConditionalOnBean(FileSystemArchive.class)
@Getter
@Setter
public class FileSystemArchiveConfiguration {
private String directory;
@Bean
public MessageDigest fileStoreDigest() throws NoSuchAlgorithmException {
return MessageDigest.getInstance("SHA-512");
}
@PostConstruct
public void init() throws IOException {
if (directory == null) {
log.info("Directory not configured take temp path");
Path tmpPath = Files.createTempDirectory("fling");
this.directory = tmpPath.toAbsolutePath().toString();
}
log.info("File store directory: {}", directory);
}
}

View file

@ -1,46 +1,48 @@
package net.friedl.fling.persistence.entities; package net.friedl.fling.persistence.entities;
import java.nio.file.Path;
import java.time.Instant; import java.time.Instant;
import java.util.UUID;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.GeneratedValue; import javax.persistence.GeneratedValue;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.ManyToOne; import javax.persistence.ManyToOne;
import javax.persistence.PrePersist;
import javax.persistence.Table; import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import javax.persistence.Version;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@Entity @Entity
@Table(name = "Artifact") @Table(name = "Artifact", uniqueConstraints = @UniqueConstraint(columnNames = {"fling_id", "path"}))
@Getter @Getter
@Setter @Setter
public class ArtifactEntity { public class ArtifactEntity {
@Id @Id
@GeneratedValue @GeneratedValue
private Long id; private UUID id;
private String name; @Column(nullable = false)
private Path path;
private Integer version; @Column(unique = true, nullable = true)
private String archiveId;
private String path; @Column(nullable = false)
private Boolean archived = false;
@Column(unique = true)
private String doi;
private Instant uploadTime;
private Long size;
@ManyToOne(optional = false) @ManyToOne(optional = false)
private FlingEntity fling; private FlingEntity fling;
@PrePersist @CreationTimestamp
private void prePersist() { private Instant creationTime;
this.uploadTime = Instant.now();
if (this.version == null) @UpdateTimestamp
this.version = -1; private Instant updateTime;
}
@Version
private Long version;
} }

View file

@ -1,16 +1,19 @@
package net.friedl.fling.persistence.entities; package net.friedl.fling.persistence.entities;
import java.time.Instant; import java.time.Instant;
import java.util.Date;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import javax.persistence.CascadeType; import javax.persistence.CascadeType;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.GeneratedValue; import javax.persistence.GeneratedValue;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.OneToMany; import javax.persistence.OneToMany;
import javax.persistence.PostPersist;
import javax.persistence.PrePersist;
import javax.persistence.Table; import javax.persistence.Table;
import javax.persistence.Version;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@ -21,50 +24,37 @@ import lombok.Setter;
public class FlingEntity { public class FlingEntity {
@Id @Id
@GeneratedValue @GeneratedValue
private Long id; private UUID id;
private String name; private String name;
private Instant creationTime;
private Instant expirationTime; private Instant expirationTime;
private Integer expirationClicks; private Integer expirationClicks;
@Column(nullable = false) @Column(nullable = false)
private Boolean directDownload; private Boolean directDownload = false;
@Column(nullable = false) @Column(nullable = false)
private Boolean allowUpload; private Boolean allowUpload = false;
@Column(nullable = false) @Column(nullable = false)
private Boolean shared; private Boolean shared = true;
@Column(unique = true, nullable = false) @Column(unique = true, nullable = false)
private String shareUrl; private String shareId;
private String authCode; private String authCode;
@OneToMany(mappedBy = "fling", cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(mappedBy = "fling", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<ArtifactEntity> artifacts; private Set<ArtifactEntity> artifacts;
@PrePersist @CreationTimestamp
private void prePersist() { private Date creationTime;
if (this.directDownload == null)
this.directDownload = false;
if (this.allowUpload == null)
this.allowUpload = false;
if (this.shared == null)
this.shared = true;
this.creationTime = Instant.now(); @UpdateTimestamp
} private Date updateTime;
@PostPersist @Version
private void postPersist() { private Long version;
System.out.println("ID: " + this.id);
System.out.println("Share Url: " + this.shareUrl);
this.shareUrl = this.id + this.shareUrl;
}
} }

View file

@ -0,0 +1,38 @@
package net.friedl.fling.persistence.entities;
import java.time.Instant;
import java.util.UUID;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Version;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "Token")
@Getter
@Setter
public class TokenEntity {
@Id
private UUID id; // Note that this is not generated to ensure randomness independent from the
// persistence provider
@Column(nullable = false)
private Boolean singleUse = true;
@Column(nullable = false)
private String token; // JWT token this token is derived from
@CreationTimestamp
private Instant creationTime;
@UpdateTimestamp
private Instant updateTime;
@Version
private Long version;
}

View file

@ -1,14 +1,10 @@
package net.friedl.fling.persistence.repositories; package net.friedl.fling.persistence.repositories;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import net.friedl.fling.persistence.entities.ArtifactEntity; import net.friedl.fling.persistence.entities.ArtifactEntity;
public interface ArtifactRepository extends JpaRepository<ArtifactEntity, Long> { public interface ArtifactRepository extends JpaRepository<ArtifactEntity, UUID> {
Optional<ArtifactEntity> findByDoi(String doi); List<ArtifactEntity> findAllByFlingId(UUID flingId);
List<ArtifactEntity> deleteByDoi(String doi);
List<ArtifactEntity> findAllByFlingId(Long flingId);
} }

View file

@ -1,15 +1,16 @@
package net.friedl.fling.persistence.repositories; package net.friedl.fling.persistence.repositories;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import net.friedl.fling.persistence.entities.FlingEntity; import net.friedl.fling.persistence.entities.FlingEntity;
public interface FlingRepository extends JpaRepository<FlingEntity, Long> { public interface FlingRepository extends JpaRepository<FlingEntity, UUID> {
Optional<FlingEntity> findByName(String name); Optional<FlingEntity> findByName(String name);
Optional<FlingEntity> findByShareUrl(String shareUrl); FlingEntity findByShareId(String shareId);
@Query("SELECT COUNT(*) FROM ArtifactEntity a, FlingEntity f where a.fling=f.id and f.id=:flingId") @Query("SELECT fe FROM FlingEntity fe JOIN ArtifactEntity ae ON fe.id=ae.fling.id WHERE ae.id=:artifactId")
Long countArtifactsById(Long flingId); FlingEntity findByArtifactId(UUID artifactId);
} }

View file

@ -0,0 +1,8 @@
package net.friedl.fling.persistence.repositories;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import net.friedl.fling.persistence.entities.TokenEntity;
public interface TokenRepository extends JpaRepository<TokenEntity, UUID> {
}

View file

@ -0,0 +1,21 @@
package net.friedl.fling.persistence.types;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
@Converter(autoApply = true)
public class PathConverter implements AttributeConverter<Path, String> {
@Override
public String convertToDatabaseColumn(Path attribute) {
return attribute.toString();
}
@Override
public Path convertToEntityAttribute(String dbData) {
return Paths.get(dbData);
}
}

View file

@ -1,89 +0,0 @@
package net.friedl.fling.security;
import java.util.NoSuchElementException;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import net.friedl.fling.security.authentication.FlingToken;
import net.friedl.fling.security.authentication.dto.UserAuthDto;
import net.friedl.fling.service.ArtifactService;
import net.friedl.fling.service.FlingService;
@Slf4j
@Service
public class AuthorizationService {
private FlingService flingService;
private ArtifactService artifactService;
@Autowired
public AuthorizationService(FlingService flingService, ArtifactService artifactService) {
this.flingService = flingService;
this.artifactService = artifactService;
}
public boolean allowUpload(Long flingId, AbstractAuthenticationToken token) {
if (!(token instanceof FlingToken))
return false;
FlingToken flingToken = (FlingToken) token;
if (flingToken.getGrantedFlingAuthority().getAuthority()
.equals(FlingAuthority.FLING_OWNER.name())) {
return true;
}
var uploadAllowed = flingService.findFlingById(flingId).orElseThrow().getAllowUpload();
return uploadAllowed && flingToken.getGrantedFlingAuthority().getFlingId().equals(flingId);
}
public boolean allowPatchingArtifact(Long artifactId, FlingToken authentication) {
var flingId = artifactService.findArtifact(artifactId).orElseThrow().getFling().getId();
return allowUpload(flingId, authentication);
}
public boolean allowFlingAccess(UserAuthDto userAuth, String shareUrl) {
return userAuth.getShareId().equals(shareUrl);
}
public boolean allowFlingAccess(Long flingId, AbstractAuthenticationToken token) {
if (!(token instanceof FlingToken))
return false;
FlingToken flingToken = (FlingToken) token;
if (flingToken.getGrantedFlingAuthority().getAuthority()
.equals(FlingAuthority.FLING_OWNER.name())) {
return true;
}
return flingToken.getGrantedFlingAuthority().getFlingId().equals(flingId);
}
public boolean allowFlingAccess(AbstractAuthenticationToken token, HttpServletRequest request) {
if (!(token instanceof FlingToken))
return false;
FlingToken flingToken = (FlingToken) token;
if (flingToken.getGrantedFlingAuthority().getAuthority()
.equals(FlingAuthority.FLING_OWNER.name())) {
return true;
}
var shareId = request.getParameter("shareId");
Long flingId;
try {
flingId = shareId != null
? flingService.findFlingByShareId(shareId).orElseThrow().getId()
: Long.parseLong(request.getParameter("flingId"));
} catch (NumberFormatException | NoSuchElementException e) {
log.warn("Invalid shareId [shareId=\"{}\"] or flingId [flingId=\"{}\"] found",
request.getParameter("shareId"), request.getParameter("flingId"));
flingId = null;
}
return flingToken.getGrantedFlingAuthority().getFlingId().equals(flingId);
}
}

View file

@ -0,0 +1,32 @@
package net.friedl.fling.security;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
public enum FlingAuthorities {
FLING_ADMIN("admin"), FLING_USER("user"), FLING_TOKEN("token");
String authority;
FlingAuthorities(String authority) {
this.authority = authority;
}
public boolean verify(String authority) {
return this.authority.equals(authority);
}
public boolean verify(AbstractAuthenticationToken authenticationToken) {
return authenticationToken.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.anyMatch(this.authority::equals);
}
public boolean verify(GrantedAuthority grantedAuthority) {
return this.authority.equals(grantedAuthority.getAuthority());
}
public String getAuthority() {
return authority;
}
}

View file

@ -1,5 +0,0 @@
package net.friedl.fling.security;
public enum FlingAuthority {
FLING_OWNER, FLING_USER
}

View file

@ -1,128 +1,149 @@
package net.friedl.fling.security; package net.friedl.fling.security;
import static net.friedl.fling.security.FlingAuthorities.FLING_ADMIN;
import static net.friedl.fling.security.FlingAuthorities.FLING_USER;
import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.config.Customizer.withDefaults;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.friedl.fling.security.authentication.JwtAuthenticationFilter; import net.friedl.fling.security.authentication.filter.BearerAuthenticationFilter;
import net.friedl.fling.security.authentication.filter.TokenAuthenticationFilter;
import net.friedl.fling.service.AuthorizationService;
@Slf4j @Slf4j
@Configuration
@EnableWebSecurity @EnableWebSecurity
@ConfigurationProperties(prefix = "fling.security")
@Getter @Getter
@Setter @Setter
public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter { public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
private JwtAuthenticationFilter jwtAuthenticationFilter; private List<String> allowedOrigins;
private TokenAuthenticationFilter tokenAuthenticationFilter;
private BearerAuthenticationFilter bearerAuthenticationFilter;
private AuthorizationService authorizationService; private AuthorizationService authorizationService;
private FlingSecurityConfiguration securityConfiguration;
@Autowired @Autowired
public FlingWebSecurityConfigurer(JwtAuthenticationFilter jwtAuthenticationFilter, public FlingWebSecurityConfigurer(
AuthorizationService authorizationService, TokenAuthenticationFilter tokenAuthenticationFilter,
FlingSecurityConfiguration securityConfiguraiton) { BearerAuthenticationFilter bearerAuthenticationFilter,
AuthorizationService authorizationService) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter; this.tokenAuthenticationFilter = tokenAuthenticationFilter;
this.bearerAuthenticationFilter = bearerAuthenticationFilter;
this.authorizationService = authorizationService; this.authorizationService = authorizationService;
this.securityConfiguration = securityConfiguraiton;
} }
@Override @Override
protected void configure(HttpSecurity http) throws Exception { protected void configure(HttpSecurity http) throws Exception {
//@formatter:off //@formatter:off
http http
.csrf().disable() .csrf().disable()
.cors(withDefaults()) .cors(withDefaults())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .headers().frameOptions().disable().and()
// Everybody can try to authenticate
/**********************************************/
/** Authentication Interceptor Configuration **/
/**********************************************/
.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(bearerAuthenticationFilter, TokenAuthenticationFilter.class)
// Do not keep authorization token in session. This would interfere with bearer authentication
// in that it is possible to authenticate without a bearer token if the session is kept.
// Turn off this confusing and non-obvious behavior.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
/*************************************/
/** API Authorization Configuration **/
/*************************************/
//! Go from most specific to more !//
//! general, as first hit counts !//
/**********************************/
/** Authorization for: /api/auth **/
/**********************************/
.authorizeRequests()
.antMatchers("/api/auth/derive")
.hasAnyAuthority(FLING_ADMIN.getAuthority(), FLING_USER.getAuthority())
.and()
.authorizeRequests() .authorizeRequests()
.antMatchers("/api/auth/**") .antMatchers("/api/auth/**")
.permitAll() .permitAll()
.and() .and()
// We need to go from most specific to more general.
// Hence, first define user permissions
/***********************************/
/** Authorization for: /api/fling **/
/***********************************/
.authorizeRequests() .authorizeRequests()
// TODO: This is still insecure since URLs are not encrypted .antMatchers(HttpMethod.GET, "/api/fling/share/{shareId}")
// TODO: iframe requests don't send the bearer, use cookie instead .access("@authorizationService.allowFlingAccessByShareId(#shareId, authentication)")
.antMatchers(HttpMethod.GET, "/api/fling/{flingId}/download/{downloadId}")
.permitAll()
.and() .and()
.authorizeRequests() .authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/artifacts/{flingId}/**") .antMatchers(HttpMethod.GET, "/api/fling/{flingId}/**")
.access("@authorizationService.allowUpload(#flingId, authentication)") .access("@authorizationService.allowFlingAccess(#flingId, authentication)")
.and()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/fling/{flingId}/artifact")
.access("@authorizationService.allowUpload(#flingId, authentication)")
.and()
// only admin can create, delete, list and modify flings
.authorizeRequests()
.antMatchers(HttpMethod.DELETE, "/api/fling/{flingId}")
.hasAnyAuthority(FLING_ADMIN.getAuthority())
.and() .and()
.authorizeRequests() .authorizeRequests()
.antMatchers(HttpMethod.PATCH, "/api/artifacts/{artifactId}") .antMatchers(HttpMethod.PUT, "/api/fling/{flingId}")
.access("@authorizationService.allowPatchingArtifact(#artifactId, authentication)") .hasAnyAuthority(FLING_ADMIN.getAuthority())
.and() .and()
.authorizeRequests() .authorizeRequests()
// TODO: This is still insecure since URLs are not encrypted .antMatchers(HttpMethod.POST, "/api/fling")
// TODO: iframe requests don't send the bearer, use cookie instead .hasAuthority(FLING_ADMIN.getAuthority())
.antMatchers("/api/artifacts/{artifactId}/{downloadId}/download")
.permitAll()
.and() .and()
.authorizeRequests() .authorizeRequests()
// TODO: Security by request parameters is just not well supported with spring security .antMatchers(HttpMethod.GET, "/api/fling")
// TODO: Change API .hasAuthority(FLING_ADMIN.getAuthority())
.regexMatchers(HttpMethod.GET, "\\/api\\/fling\\?(shareId=|flingId=)[a-zA-Z0-9]+") .and()
.access("@authorizationService.allowFlingAccess(authentication, request)")
/***************************************/
/** Authorization for: /api/artifacts **/
/***************************************/
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/artifacts/{artifactId}/**")
.access("@authorizationService.allowArtifactAccess(#artifactId, authentication)")
.and() .and()
.authorizeRequests() .authorizeRequests()
// TODO: Security by request parameters is just not well supported with spring security .antMatchers(HttpMethod.POST, "/api/artifacts/{artifactId}/data")
// TODO: Change API .access("@authorizationService.allowArtifactUpload(#artifactId, authentication)")
.regexMatchers(HttpMethod.GET, "\\/api\\/artifacts\\?(shareId=|flingId=)[a-zA-Z0-9]+")
.access("@authorizationService.allowFlingAccess(authentication, request)")
.and() .and()
.authorizeRequests() .authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/fling/{flingId}/**") .antMatchers(HttpMethod.DELETE, "/api/artifacts/{artifactId}")
.access("@authorizationService.allowFlingAccess(#flingId, authentication)") .access("@authorizationService.allowArtifactUpload(#artifactId, authentication)");
.and()
// And lastly, the owner is allowed everything
.authorizeRequests()
.antMatchers("/api/**")
.hasAuthority(FlingAuthority.FLING_OWNER.name());
//@formatter:on //@formatter:on
} }
private RequestMatcher modificationMethodsAntMatcher(String antPattern) {
return multiMethodAntMatcher(antPattern,
HttpMethod.PATCH, HttpMethod.PUT,
HttpMethod.POST, HttpMethod.DELETE);
}
private RequestMatcher multiMethodAntMatcher(String antPattern, HttpMethod... httpMethods) {
List<RequestMatcher> antMatchers = Arrays.stream(httpMethods)
.map(m -> new AntPathRequestMatcher(antPattern, m.toString()))
.collect(Collectors.toList());
return new OrRequestMatcher(antMatchers);
}
@Bean @Bean
public CorsConfigurationSource corsConfigurationSource() { public CorsConfigurationSource corsConfigurationSource() {
// see https://stackoverflow.com/a/43559266 // see https://stackoverflow.com/a/43559266
log.info("Allowed origins: {}", securityConfiguration.getAllowedOrigins()); log.info("Allowed origins: {}", allowedOrigins);
CorsConfiguration configuration = new CorsConfiguration(); CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(securityConfiguration.getAllowedOrigins()); configuration.setAllowedOrigins(allowedOrigins);
configuration.setAllowedMethods(List.of("*")); configuration.setAllowedMethods(List.of("*"));
// setAllowCredentials(true) is important, otherwise: // setAllowCredentials(true) is important, otherwise:

View file

@ -1,31 +0,0 @@
package net.friedl.fling.security.authentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import net.friedl.fling.security.authentication.dto.OwnerAuthDto;
import net.friedl.fling.security.authentication.dto.UserAuthDto;
@RestController
@RequestMapping("/api")
public class AuthenticationController {
private AuthenticationService authenticationService;
@Autowired
public AuthenticationController(AuthenticationService authenticationService) {
this.authenticationService = authenticationService;
}
@PostMapping("/auth/owner")
public String authenticateOwner(@RequestBody OwnerAuthDto ownerAuthDto) {
return authenticationService.authenticate(ownerAuthDto);
}
@PostMapping("/auth/user")
public String authenticateUser(@RequestBody UserAuthDto userAuthDto) {
return authenticationService.authenticate(userAuthDto);
}
}

View file

@ -1,100 +0,0 @@
package net.friedl.fling.security.authentication;
import java.security.Key;
import java.time.Instant;
import java.util.Date;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import net.friedl.fling.security.FlingAuthority;
import net.friedl.fling.security.FlingSecurityConfiguration;
import net.friedl.fling.security.authentication.dto.OwnerAuthDto;
import net.friedl.fling.security.authentication.dto.UserAuthDto;
import net.friedl.fling.service.FlingService;
@Service
public class AuthenticationService {
private FlingService flingService;
private JwtParser jwtParser;
private Key signingKey;
private FlingSecurityConfiguration securityConfig;
@Autowired
public AuthenticationService(JwtParser jwtParser, Key signingKey, FlingService flingService,
FlingSecurityConfiguration securityConfig) {
this.flingService = flingService;
this.jwtParser = jwtParser;
this.signingKey = signingKey;
this.securityConfig = securityConfig;
}
public String authenticate(OwnerAuthDto ownerAuth) {
if (!securityConfig.getAdminUser().equals(ownerAuth.getUsername())) {
throw new AccessDeniedException("Wrong credentials");
}
if (!securityConfig.getAdminPassword().equals(ownerAuth.getPassword())) {
throw new AccessDeniedException("Wrong credentials");
}
return makeBaseBuilder()
.setSubject("owner")
.compact();
}
public String authenticate(UserAuthDto userAuth) {
var fling = flingService.findFlingByShareId(userAuth.getShareId())
.orElseThrow();
String authCode = userAuth.getCode();
if (!flingService.hasAuthCode(fling.getId(), authCode)) {
throw new AccessDeniedException("Wrong fling code");
}
return makeBaseBuilder()
.setSubject("user")
.claim("sid", fling.getShareUrl())
.compact();
}
public Authentication parseAuthentication(String token) {
Claims claims = parseClaims(token);
FlingAuthority authority;
Long flingId;
switch (claims.getSubject()) {
case "owner":
authority = FlingAuthority.FLING_OWNER;
flingId = null;
break;
case "user":
authority = FlingAuthority.FLING_USER;
var sid = claims.get("sid", String.class);
flingId = flingService.findFlingByShareId(sid).orElseThrow().getId();
break;
default:
throw new BadCredentialsException("Invalid token");
}
return new FlingToken(new GrantedFlingAuthority(authority, flingId));
}
private JwtBuilder makeBaseBuilder() {
return Jwts.builder()
.setIssuedAt(Date.from(Instant.now()))
.setExpiration(Date.from(Instant.now().plusSeconds(securityConfig.getJwtExpiration())))
.signWith(signingKey);
}
private Claims parseClaims(String token) {
return jwtParser.parseClaimsJws(token).getBody();
}
}

View file

@ -1,25 +1,42 @@
package net.friedl.fling.security.authentication; package net.friedl.fling.security.authentication;
import java.util.List; import java.util.List;
import java.util.UUID;
import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import net.friedl.fling.security.authentication.authorities.FlingAdminAuthority;
import net.friedl.fling.security.authentication.authorities.FlingUserAuthority;
public class FlingToken extends AbstractAuthenticationToken { public class FlingToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = -1112423505610346583L; private static final long serialVersionUID = -1112423505610346583L;
private GrantedFlingAuthority grantedFlingAuthority; private String token;
public FlingToken(GrantedFlingAuthority authority) { public FlingToken(List<GrantedAuthority> authorities, String token) {
super(List.of(authority)); super(authorities);
this.grantedFlingAuthority = authority; this.token = token;
} }
public GrantedFlingAuthority getGrantedFlingAuthority() { public boolean authorizedForFling(UUID id) {
return this.grantedFlingAuthority; for (GrantedAuthority grantedAuthority : getAuthorities()) {
if (grantedAuthority instanceof FlingAdminAuthority) {
return true;
}
if (grantedAuthority instanceof FlingUserAuthority) {
UUID grantedFlingId = ((FlingUserAuthority) grantedAuthority).getFlingId();
if (grantedFlingId.equals(id)) {
return true;
}
}
}
return false;
} }
@Override @Override
public Object getCredentials() { public String getCredentials() {
return null; return this.token;
} }
@Override @Override

View file

@ -1,32 +0,0 @@
package net.friedl.fling.security.authentication;
import org.springframework.security.core.GrantedAuthority;
import net.friedl.fling.security.FlingAuthority;
/**
* Authority granting access to a fling
*
* @author Armin Friedl <dev@friedl.net>
*/
public class GrantedFlingAuthority implements GrantedAuthority {
private static final long serialVersionUID = -1552301479158714777L;
private FlingAuthority authority;
private Long flingId;
public GrantedFlingAuthority(FlingAuthority authority, Long flingId) {
this.authority = authority;
this.flingId = flingId;
}
public Long getFlingId() {
return this.flingId;
}
@Override
public String getAuthority() {
return authority.name();
}
}

View file

@ -0,0 +1,15 @@
package net.friedl.fling.security.authentication.authorities;
import org.springframework.security.core.GrantedAuthority;
import net.friedl.fling.security.FlingAuthorities;
public class FlingAdminAuthority implements GrantedAuthority {
private static final long serialVersionUID = -4605768612393081070L;
@Override
public String getAuthority() {
return FlingAuthorities.FLING_ADMIN.getAuthority();
}
}

View file

@ -0,0 +1,25 @@
package net.friedl.fling.security.authentication.authorities;
import java.util.UUID;
import org.springframework.security.core.GrantedAuthority;
import net.friedl.fling.security.FlingAuthorities;
public class FlingUserAuthority implements GrantedAuthority {
private static final long serialVersionUID = -1814514234042184275L;
private UUID flingId;
public FlingUserAuthority(UUID flingId) {
this.flingId = flingId;
}
@Override
public String getAuthority() {
return FlingAuthorities.FLING_USER.getAuthority();
}
public UUID getFlingId() {
return flingId;
}
}

View file

@ -1,9 +0,0 @@
package net.friedl.fling.security.authentication.dto;
import lombok.Data;
@Data
public class OwnerAuthDto {
private String username;
private String password;
}

View file

@ -1,9 +0,0 @@
package net.friedl.fling.security.authentication.dto;
import lombok.Data;
@Data
public class UserAuthDto {
String shareId;
String code;
}

View file

@ -1,28 +1,31 @@
package net.friedl.fling.security.authentication; package net.friedl.fling.security.authentication.filter;
import java.io.IOException; import java.io.IOException;
import java.util.stream.Collectors;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.friedl.fling.security.authentication.FlingToken;
import net.friedl.fling.service.AuthenticationService;
@Slf4j @Slf4j
@Component @Component
public class JwtAuthenticationFilter extends OncePerRequestFilter { public class BearerAuthenticationFilter extends OncePerRequestFilter {
private static final String TOKEN_PREFIX = "Bearer "; private static final String TOKEN_PREFIX = "Bearer ";
private static final String HEADER_STRING = "Authorization"; private static final String HEADER_STRING = "Authorization";
private AuthenticationService authenticationService; private AuthenticationService authenticationService;
@Autowired @Autowired
public JwtAuthenticationFilter(AuthenticationService authenticationService) { public BearerAuthenticationFilter(AuthenticationService authenticationService) {
this.authenticationService = authenticationService; this.authenticationService = authenticationService;
} }
@ -34,7 +37,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
String header = request.getHeader(HEADER_STRING); String header = request.getHeader(HEADER_STRING);
if (header == null || !header.startsWith(TOKEN_PREFIX)) { if (header == null || !header.startsWith(TOKEN_PREFIX)) {
log.warn("Could not find bearer token. No JWT authentication."); log.info("Anonymous request for {} {}{}", request.getMethod(), request.getRequestURL(),
request.getQueryString() != null ? "?" + request.getQueryString() : "");
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
@ -44,8 +48,12 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
SecurityContext securityContext = SecurityContextHolder.getContext(); SecurityContext securityContext = SecurityContextHolder.getContext();
if (securityContext.getAuthentication() == null) { if (securityContext.getAuthentication() == null) {
Authentication authentication = authenticationService.parseAuthentication(authToken); log.info("Authenticating request for {} {}{}", request.getMethod(), request.getRequestURL(),
securityContext.setAuthentication(authentication); request.getQueryString() != null ? "?" + request.getQueryString() : "");
FlingToken token = authenticationService.parseJwtAuthentication(authToken);
log.info("Authenticated as {}", token.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")));
securityContext.setAuthentication(token);
} }
filterChain.doFilter(request, response); filterChain.doFilter(request, response);

View file

@ -0,0 +1,59 @@
package net.friedl.fling.security.authentication.filter;
import java.io.IOException;
import java.util.stream.Collectors;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import lombok.extern.slf4j.Slf4j;
import net.friedl.fling.security.authentication.FlingToken;
import net.friedl.fling.service.AuthenticationService;
@Slf4j
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private AuthenticationService authenticationService;
@Autowired
public TokenAuthenticationFilter(AuthenticationService authenticationService) {
this.authenticationService = authenticationService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String derivedToken = request.getParameter("derivedToken");
if (derivedToken == null) {
log.info("No derived token in request for {} {}{}", request.getMethod(),
request.getRequestURL(),
request.getQueryString() != null ? "?" + request.getQueryString() : "");
filterChain.doFilter(request, response);
return;
}
SecurityContext securityContext = SecurityContextHolder.getContext();
if (securityContext.getAuthentication() == null) {
log.info("Authenticating request for {} {}{}", request.getMethod(), request.getRequestURL(),
request.getQueryString() != null ? "?" + request.getQueryString() : "");
FlingToken token = authenticationService.parseDerivedToken(derivedToken);
log.info("Authenticated as {}", token.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")));
securityContext.setAuthentication(token);
}
filterChain.doFilter(request, response);
}
}

View file

@ -1,89 +1,78 @@
package net.friedl.fling.service; package net.friedl.fling.service;
import java.io.InputStream; import java.io.IOException;
import java.util.List; import java.util.UUID;
import java.util.Map;
import java.util.Optional;
import javax.transaction.Transactional; import javax.transaction.Transactional;
import javax.validation.constraints.NotNull;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.json.JsonParser;
import org.springframework.boot.json.JsonParserFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import net.friedl.fling.model.dto.ArtifactDto; import net.friedl.fling.model.dto.ArtifactDto;
import net.friedl.fling.model.mapper.ArtifactMapper; import net.friedl.fling.model.mapper.ArtifactMapper;
import net.friedl.fling.persistence.archive.Archive;
import net.friedl.fling.persistence.archive.ArchiveException;
import net.friedl.fling.persistence.entities.ArtifactEntity; import net.friedl.fling.persistence.entities.ArtifactEntity;
import net.friedl.fling.persistence.entities.FlingEntity;
import net.friedl.fling.persistence.repositories.ArtifactRepository; import net.friedl.fling.persistence.repositories.ArtifactRepository;
import net.friedl.fling.persistence.repositories.FlingRepository; import net.friedl.fling.persistence.repositories.FlingRepository;
import net.friedl.fling.service.archive.ArchiveService;
@Slf4j
@Service @Service
@Transactional @Transactional
public class ArtifactService { public class ArtifactService {
private FlingRepository flingRepository;
private ArtifactRepository artifactRepository; private ArtifactRepository artifactRepository;
private FlingRepository flingRepository;
private ArtifactMapper artifactMapper; private ArtifactMapper artifactMapper;
private Archive archive; private ArchiveService archiveService;
@Autowired @Autowired
public ArtifactService(ArtifactRepository artifactRepository, FlingRepository flingRepository, public ArtifactService(ArtifactRepository artifactRepository, FlingRepository flingRepository,
ArtifactMapper artifactMapper, Archive archive) { ArtifactMapper artifactMapper, ArchiveService archiveService) {
this.artifactRepository = artifactRepository; this.artifactRepository = artifactRepository;
this.flingRepository = flingRepository; this.flingRepository = flingRepository;
this.artifactMapper = artifactMapper; this.artifactMapper = artifactMapper;
this.archive = archive; this.archiveService = archiveService;
} }
public List<ArtifactDto> findAllArtifacts(Long flingId) { /**
return artifactMapper.map(artifactRepository.findAllByFlingId(flingId)); * Fetch an {@link ArtifactDto} by id. Must be called with a valid artifact id, otherwise bails
* out with a {@link RuntimeException}. Synchronization must be done on client side.
*
* @param id A valid {@link UUID} for an existing entity in the database. Not null.
* @return The ArtifactDto corresponding to the {@code id}
*/
@NotNull
public ArtifactDto getById(@NotNull UUID id) {
return artifactMapper.map(artifactRepository.getOne(id));
} }
public ArtifactDto storeArtifact(Long flingId, InputStream artifact) throws ArchiveException { /**
var flingEntity = flingRepository.findById(flingId).orElseThrow(); * Create a new {@link ArtifactEntity} from {@code artifactDto} for the fling {@code flingId}.
var archiveId = archive.store(artifact); *
* @param flingId Id of an existing {@link FlingEntity}
* @param artifactDto The data for the new {@link ArtifactEntity}
* @return The newly created artifact
*/
public ArtifactDto create(UUID flingId, ArtifactDto artifactDto) {
FlingEntity flingEntity = flingRepository.getOne(flingId);
ArtifactEntity artifactEntity = new ArtifactEntity(); log.debug("Creating new ArtifactEntity for ArtifactDto[.path={}]", artifactDto.getPath());
artifactEntity.setDoi(archiveId); ArtifactEntity artifactEntity = artifactMapper.map(artifactDto);
artifactEntity.setFling(flingEntity); artifactEntity.setFling(flingEntity);
artifactEntity = artifactRepository.save(artifactEntity);
artifactRepository.save(artifactEntity);
return artifactMapper.map(artifactEntity); return artifactMapper.map(artifactEntity);
} }
public Optional<ArtifactDto> findArtifact(Long artifactId) { /**
return artifactMapper.map(artifactRepository.findById(artifactId)); * Deletes an artifact identified by {@code id}. NOOP if the artifact cannot be found.
} *
* @param id An {@link UUID} that identifies the artifact
public ArtifactDto mergeArtifact(Long artifactId, String body) { * @throws IOException If the deletion failed
JsonParser jsonParser = JsonParserFactory.getJsonParser(); */
Map<String, Object> parsedBody = jsonParser.parseMap(body); public void delete(UUID id) throws IOException {
archiveService.deleteArtifact(id);
artifactRepository.findById(artifactId) artifactRepository.deleteById(id);
// map entity to dto log.info("Deleted artifact {}", id);
.map(artifactMapper::map)
// merge parsedBody into dto
.map(a -> artifactMapper.merge(a, parsedBody))
// map dto to entity
.map(artifactMapper::map)
.ifPresent(artifactRepository::save);
return artifactMapper.map(artifactRepository.getOne(artifactId));
}
public void deleteArtifact(Long artifactId) throws ArchiveException {
var doi = artifactRepository.getOne(artifactId).getDoi();
artifactRepository.deleteById(artifactId);
archive.remove(doi);
}
public String generateDownloadId(Long artifactId) {
// TODO: This id is not secured! Generate temporary download id
return artifactRepository.getOne(artifactId).getDoi();
}
public InputStream downloadArtifact(String downloadId) throws ArchiveException {
return archive.get(downloadId);
} }
} }

View file

@ -0,0 +1,179 @@
package net.friedl.fling.service;
import java.security.Key;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import javax.persistence.EntityNotFoundException;
import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import net.friedl.fling.model.dto.AdminAuthDto;
import net.friedl.fling.model.dto.UserAuthDto;
import net.friedl.fling.persistence.entities.FlingEntity;
import net.friedl.fling.persistence.entities.TokenEntity;
import net.friedl.fling.persistence.repositories.FlingRepository;
import net.friedl.fling.persistence.repositories.TokenRepository;
import net.friedl.fling.security.authentication.FlingToken;
import net.friedl.fling.security.authentication.authorities.FlingAdminAuthority;
import net.friedl.fling.security.authentication.authorities.FlingUserAuthority;
@Slf4j
@Service
public class AuthenticationService {
private JwtParser jwtParser;
private Key jwtSigningKey;
private FlingRepository flingRepository;
private TokenRepository tokenRepository;
private PasswordEncoder passwordEncoder;
@Value("${fling.security.admin-name}")
private String adminName;
@Value("${fling.security.admin-password}")
private String adminPassword;
@Value("${fling.security.jwt-expiration}")
private Long jwtExpiration;
@Autowired
public AuthenticationService(JwtParser jwtParser, Key jwtSigningKey,
PasswordEncoder passwordEncoder, FlingRepository flingRepository,
TokenRepository tokenRepository) {
this.jwtParser = jwtParser;
this.jwtSigningKey = jwtSigningKey;
this.passwordEncoder = passwordEncoder;
this.flingRepository = flingRepository;
this.tokenRepository = tokenRepository;
}
public Optional<String> authenticate(AdminAuthDto adminAuth) {
log.info("Authenticating {}", adminAuth.getAdminName());
if (!adminName.equals(adminAuth.getAdminName())) {
log.debug("Authentication failed for {}", adminAuth.getAdminName());
return Optional.empty();
}
if (!adminPassword.equals(adminAuth.getAdminPassword())) {
log.debug("Authentication failed for {}", adminAuth.getAdminName());
return Optional.empty();
}
log.debug("Authentication successful for {}", adminAuth.getAdminName());
return Optional.of(
getJwtBuilder()
.setSubject("admin")
.compact());
}
public Optional<String> authenticate(UserAuthDto userAuth) {
log.info("Authenticating for fling [.shareId={}]", userAuth.getShareId());
FlingEntity flingEntity = flingRepository.findByShareId(userAuth.getShareId());
if (flingEntity == null) {
throw new EntityNotFoundException("No entity for shareId=" + userAuth.getShareId());
}
String providedAuthCode = userAuth.getAuthCode();
String actualAuthCodeHash = flingEntity.getAuthCode();
Boolean isProtected = StringUtils.hasText(actualAuthCodeHash);
if(!isProtected) log.debug("No protection set for fling [.shareId={}]");
if (isProtected && !passwordEncoder.matches(providedAuthCode, actualAuthCodeHash)) {
log.debug("Authentication failed for fling [.shareId={}]", userAuth.getShareId());
return Optional.empty();
}
log.debug("Authentication successful for fling [.shareId={}]", userAuth.getShareId());
return Optional.of(
getJwtBuilder()
.setSubject("user")
.claim("shareId", flingEntity.getShareId())
.claim("id", flingEntity.getId())
.compact());
}
public FlingToken parseJwtAuthentication(String token) {
Claims claims = jwtParser.parseClaimsJws(token).getBody();
switch (claims.getSubject()) {
case "admin":
return new FlingToken(List.of(new FlingAdminAuthority()), token);
case "user":
UUID grantedFlingId = UUID.fromString(claims.get("id", String.class));
return new FlingToken(List.of(new FlingUserAuthority(grantedFlingId)), token);
default:
throw new BadCredentialsException("Invalid token");
}
}
/**
* Creates a new JwtBuilder. A new builder must be constructed for each JWT creation, because the
* builder keeps its state.
*
* @return A new JwtBuilder with basic default configuration.
*/
private JwtBuilder getJwtBuilder() {
return Jwts.builder()
.setIssuedAt(Date.from(Instant.now()))
.setExpiration(Date.from(Instant.now().plusSeconds(jwtExpiration)))
.signWith(jwtSigningKey);
}
/**
* Creates a derived token with the given settings. Note that the returned string is opaque and
* should not not be interpreted in any way but only used as is.
*
* @param singleUse Whether this token should be deleted after a single use
* @return An opaque string representing the token
*/
@Transactional
public String deriveToken(Boolean singleUse) {
UUID id = UUID.randomUUID();
TokenEntity tokenEntity = new TokenEntity();
tokenEntity.setId(id);
if (singleUse != null) {
tokenEntity.setSingleUse(singleUse);
}
SecurityContext securityContext = SecurityContextHolder.getContext();
if (securityContext.getAuthentication() instanceof FlingToken) {
FlingToken flingToken = (FlingToken) securityContext.getAuthentication();
tokenEntity.setToken(flingToken.getCredentials());
} else {
// This should be prevented in FlingWebSecurityConfigurer
throw new IllegalStateException("Cannot derive token from current authentication");
}
tokenRepository.save(tokenEntity);
return id.toString();
}
@Transactional
public FlingToken parseDerivedToken(String derivedToken) {
TokenEntity tokenEntity = tokenRepository.getOne(UUID.fromString(derivedToken));
FlingToken flingToken = parseJwtAuthentication(tokenEntity.getToken());
if (tokenEntity.getSingleUse()) {
tokenRepository.delete(tokenEntity);
}
return flingToken;
}
}

View file

@ -0,0 +1,97 @@
package net.friedl.fling.service;
import java.util.UUID;
import javax.persistence.EntityNotFoundException;
import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import net.friedl.fling.persistence.entities.FlingEntity;
import net.friedl.fling.persistence.repositories.FlingRepository;
import net.friedl.fling.security.FlingAuthorities;
import net.friedl.fling.security.authentication.FlingToken;
@Slf4j
@Service
@Transactional
public class AuthorizationService {
private FlingRepository flingRepository;
@Autowired
public AuthorizationService(FlingRepository flingRepository) {
this.flingRepository = flingRepository;
}
public boolean allowUpload(UUID flingId, AbstractAuthenticationToken token) {
if (!(token instanceof FlingToken)) {
log.debug("Token of type {} not allowed. Authentication denied.", token.getClass());
return false;
}
if (FlingAuthorities.FLING_ADMIN.verify(token)) {
log.debug("Owner authorized for upload fling[.id={}]", flingId);
return true;
}
FlingEntity flingEntity = flingRepository.getOne(flingId);
if (flingEntity.getAllowUpload() == null || !flingEntity.getAllowUpload()) {
log.debug("Fling[.id={}] does not not allow uploads");
return false;
}
FlingToken flingToken = (FlingToken) token;
if (flingToken.authorizedForFling(flingId)) {
log.debug("User authorized for upload fling[.id={}]", flingId);
return true;
}
log.info("User not authorized for upload fling[.id={}]", flingId);
return false;
}
public boolean allowFlingAccess(UUID flingId, AbstractAuthenticationToken token) {
if (!(token instanceof FlingToken)) {
log.debug("Token of type {} not allowed. Authentication denied.", token.getClass());
return false;
}
if (FlingAuthorities.FLING_ADMIN.verify(token)) {
log.debug("Owner authorized for fling access [id = {}]", flingId);
return true;
}
FlingToken flingToken = (FlingToken) token;
if (flingToken.authorizedForFling(flingId)) {
log.debug("User authorized for fling access [id = {}]");
return true;
}
log.info("User not authorized to access fling[.id={}]", flingId);
return false;
}
public boolean allowFlingAccessByShareId(String shareId, AbstractAuthenticationToken token) {
if (FlingAuthorities.FLING_ADMIN.verify(token)) {
log.debug("Owner authorized for fling access [shareId = {}]", shareId);
return true;
}
FlingEntity flingEntity = flingRepository.findByShareId(shareId);
if (flingEntity == null) {
throw new EntityNotFoundException("No entity for shareId=" + shareId);
}
return allowFlingAccess(flingEntity.getId(), token);
}
public boolean allowArtifactAccess(UUID artifactId, AbstractAuthenticationToken token) {
FlingEntity flingEntity = flingRepository.findByArtifactId(artifactId);
return allowFlingAccess(flingEntity.getId(), token);
}
public boolean allowArtifactUpload(UUID artifactId, AbstractAuthenticationToken token) {
FlingEntity flingEntity = flingRepository.findByArtifactId(artifactId);
return allowUpload(flingEntity.getId(), token);
}
}

View file

@ -1,36 +1,26 @@
package net.friedl.fling.service; package net.friedl.fling.service;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.util.Base64; import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Set;
import java.util.function.Consumer; import java.util.UUID;
import java.util.function.Supplier; import javax.persistence.EntityNotFoundException;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.transaction.Transactional; import javax.transaction.Transactional;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.util.Pair;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.security.crypto.keygen.KeyGenerators; import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.friedl.fling.model.dto.ArtifactDto;
import net.friedl.fling.model.dto.FlingDto; import net.friedl.fling.model.dto.FlingDto;
import net.friedl.fling.model.mapper.ArtifactMapper;
import net.friedl.fling.model.mapper.FlingMapper; import net.friedl.fling.model.mapper.FlingMapper;
import net.friedl.fling.persistence.archive.Archive;
import net.friedl.fling.persistence.archive.ArchiveException;
import net.friedl.fling.persistence.entities.ArtifactEntity;
import net.friedl.fling.persistence.entities.FlingEntity; import net.friedl.fling.persistence.entities.FlingEntity;
import net.friedl.fling.persistence.repositories.FlingRepository; import net.friedl.fling.persistence.repositories.FlingRepository;
import net.friedl.fling.service.archive.ArchiveService;
@Slf4j @Slf4j
@Service @Service
@ -39,135 +29,115 @@ public class FlingService {
private FlingRepository flingRepository; private FlingRepository flingRepository;
private FlingMapper flingMapper; private FlingMapper flingMapper;
private Archive archive; private ArtifactMapper artifactMapper;
private MessageDigest keyHashDigest; private ArchiveService archiveService;
private PasswordEncoder passwordEncoder;
@Autowired @Autowired
public FlingService(FlingRepository flingRepository, FlingMapper flingMapper, Archive archive, public FlingService(FlingRepository flingRepository, FlingMapper flingMapper,
MessageDigest keyHashDigest) { ArtifactMapper artifactMapper,
ArchiveService archiveService, PasswordEncoder passwordEcoder) {
this.flingRepository = flingRepository; this.flingRepository = flingRepository;
this.flingMapper = flingMapper; this.flingMapper = flingMapper;
this.archive = archive; this.artifactMapper = artifactMapper;
this.keyHashDigest = keyHashDigest; this.archiveService = archiveService;
this.passwordEncoder = passwordEcoder;
} }
/**
* Retrieves a list of all flings
*
* @return A list of all flings
*/
public List<FlingDto> findAll() { public List<FlingDto> findAll() {
return flingMapper.map(flingRepository.findAll()); return flingMapper.mapEntities(flingRepository.findAll());
} }
public Long createFling(FlingDto flingDto) { /**
if (!StringUtils.hasText(flingDto.getShareUrl())) { * Get a fling by id
flingDto.setShareUrl(generateShareUrl()); *
* @param id Id of the fling. Must exist.
* @return The fling
*/
public FlingDto getById(UUID id) {
return flingMapper.map(flingRepository.getOne(id));
}
/**
* Creates a new fling entity from {@code flingDto}
*
* @param flingDto Base data from which the new fling should be created
* @return The created fling
*/
public FlingDto create(FlingDto flingDto) {
log.debug("Creating new fling");
FlingEntity flingEntity = flingMapper.map(flingDto);
if (!StringUtils.hasText(flingEntity.getShareId())) {
log.debug("No share id set. Generating random share id");
flingEntity.setShareId(generateShareId());
}
if (StringUtils.hasText(flingEntity.getAuthCode())) {
log.debug("Hashing authentication code for {}", flingEntity.getId());
flingEntity.setAuthCode(hashAuthCode(flingDto.getAuthCode()));
} }
var flingEntity = flingMapper.map(flingDto);
flingEntity.setAuthCode(hashKey(flingEntity.getAuthCode()));
flingEntity = flingRepository.save(flingEntity); flingEntity = flingRepository.save(flingEntity);
return flingEntity.getId(); log.debug("Created new fling {}", flingEntity.getId());
return flingMapper.map(flingEntity);
} }
public Boolean existsShareUrl(String shareUrl) { public FlingDto getByShareId(String shareId) {
return !flingRepository.findByShareUrl(shareUrl).isEmpty(); FlingEntity flingEntity = flingRepository.findByShareId(shareId);
if (flingEntity == null) {
throw new EntityNotFoundException("No entity for shareId=" + shareId);
}
return flingMapper.map(flingEntity);
} }
public void mergeFling(Long flingId, FlingDto flingDto) { public void delete(UUID id) throws IOException {
var flingEntity = flingRepository.getOne(flingId); archiveService.deleteFling(id);
flingRepository.deleteById(id);
mergeNonEmpty(flingDto::getAllowUpload, flingEntity::setAllowUpload); log.debug("Deleted fling {}", id);
mergeNonEmpty(flingDto::getDirectDownload, flingEntity::setDirectDownload);
mergeWithEmpty(flingDto::getExpirationClicks, flingEntity::setExpirationClicks);
mergeWithEmpty(flingDto::getExpirationTime, flingEntity::setExpirationTime);
mergeNonEmpty(flingDto::getName, flingEntity::setName);
mergeNonEmpty(flingDto::getShared, flingEntity::setShared);
mergeNonEmpty(flingDto::getShareUrl, flingEntity::setShareUrl);
mergeWithEmpty(() -> hashKey(flingDto.getAuthCode()), flingEntity::setAuthCode);
} }
public Optional<FlingDto> findFlingById(Long flingId) { public Set<ArtifactDto> getArtifacts(UUID id) {
return flingMapper.map(flingRepository.findById(flingId)); FlingEntity flingEntity = flingRepository.getOne(id);
Set<ArtifactDto> artifactDto = artifactMapper.mapEntities(flingEntity.getArtifacts());
return artifactDto == null ? Set.of() : artifactDto;
} }
public Optional<FlingDto> findFlingByShareId(String shareUrl) { public boolean validateAuthCode(UUID id, String authCode) {
return flingMapper.map(flingRepository.findByShareUrl(shareUrl)); FlingEntity flingEntity = flingRepository.getOne(id);
} if (StringUtils.hasText(flingEntity.getAuthCode()) != StringUtils.hasText(authCode)) {
return false; // only one of them is empty; implicit null safety check
public void deleteFlingById(Long flingId) {
flingRepository.deleteById(flingId);
}
public boolean hasAuthCode(Long flingId, String authCode) {
var fling = flingRepository.getOne(flingId);
if (!StringUtils.hasText(fling.getAuthCode()))
return true;
return fling.getAuthCode().equals(hashKey(authCode));
}
public String getShareName(String shareUrl) {
FlingEntity flingEntity = flingRepository.findByShareUrl(shareUrl).orElseThrow();
if (flingEntity.getArtifacts().size() > 1)
return flingEntity.getName();
else if (flingEntity.getArtifacts().size() == 1)
return flingEntity.getArtifacts().stream().findFirst().get().getName();
return null;
}
public Long countArtifacts(Long flingId) {
return flingRepository.countArtifactsById(flingId);
}
public Long getFlingSize(Long flingId) {
var fling = flingRepository.getOne(flingId);
return fling.getArtifacts().stream()
.map(ae -> ae.getSize())
.reduce(0L, (acc, as) -> acc + as);
}
public String packageFling(Long flingId) throws IOException, ArchiveException {
var fling = flingRepository.getOne(flingId);
var tempFile = Files.createTempFile(Long.toString(flingId), ".zip");
try (var zipStream = new ZipOutputStream(new FileOutputStream(tempFile.toFile()))) {
zipStream.setLevel(Deflater.BEST_SPEED);
for (ArtifactEntity artifactEntity : fling.getArtifacts()) {
ZipEntry ze = new ZipEntry(artifactEntity.getName());
zipStream.putNextEntry(ze);
var artifactStream = archive.get(artifactEntity.getDoi());
try (var archiveEntryStream = new BufferedInputStream(artifactStream)) {
int b;
while ((b = archiveEntryStream.read()) != -1) {
zipStream.write(b);
}
} finally {
zipStream.closeEntry();
}
}
} }
return tempFile.getFileName().toString(); boolean valid = flingEntity.getAuthCode().equals(hashAuthCode(authCode));
log.debug("Provided authentication for {} is {} valid", id, valid ? "" : "not");
return valid;
} }
public Pair<InputStream, Long> downloadFling(String fileId) throws IOException, ArchiveException { private String hashAuthCode(String authCode) {
var tempFile = Paths.get(System.getProperty("java.io.tmpdir"), fileId).toFile(); if (!StringUtils.hasText(authCode)) return null;
String hash = passwordEncoder.encode(authCode);
var archiveLength = tempFile.length(); log.debug("Hashed authentication code to {}", hash);
var archiveStream = new FileInputStream(tempFile); return hash;
return Pair.of(archiveStream, archiveLength);
} }
public String generateShareUrl() { /**
var key = KeyGenerators * Generates a URL safe share id
*
* @return A random URL safe share id
*/
private String generateShareId() {
byte[] key = KeyGenerators
.secureRandom(16) .secureRandom(16)
.generateKey(); .generateKey();
return Base64.getUrlEncoder().encodeToString(key) String shareId = Base64.getUrlEncoder().encodeToString(key)
// replace all special chars [=-_] in RFC 4648 // replace all special chars [=-_] in RFC 4648
// "URL and Filename safe" table with characters from // "URL and Filename safe" table with characters from
// [A-Za-z0-9]. Hence, the generated share url will only consist // [A-Za-z0-9]. Hence, the generated share url will only consist
@ -175,23 +145,24 @@ public class FlingService {
.replace('=', 'q') .replace('=', 'q')
.replace('_', 'u') .replace('_', 'u')
.replace('-', 'd'); .replace('-', 'd');
log.debug("Generated share id {}", shareId);
return shareId;
} }
public String hashKey(String key) { public FlingDto replace(UUID id, @Valid FlingDto flingDto) {
if (!StringUtils.hasText(key)) FlingEntity flingEntity = flingRepository.getOne(id);
return null; flingEntity.setId(id);
flingEntity.setAllowUpload(flingDto.getAllowUpload());
return new String(Hex.encode(keyHashDigest.digest(key.getBytes()))); flingEntity.setDirectDownload(flingDto.getDirectDownload());
} flingEntity.setShared(flingDto.getShared());
flingEntity.setExpirationClicks(flingDto.getExpirationClicks());
private <T> void mergeNonEmpty(Supplier<T> sup, Consumer<T> con) { flingEntity.setExpirationTime(flingDto.getExpirationTime());
T r = sup.get(); flingEntity.setName(flingDto.getName());
if (r != null) flingEntity.setShareId(flingDto.getShareId());
con.accept(r); if (!flingDto.getAuthCode().equals(flingEntity.getAuthCode())) {
} flingEntity.setAuthCode(hashAuthCode(flingDto.getAuthCode()));
}
private <T> void mergeWithEmpty(Supplier<T> sup, Consumer<T> con) { return flingMapper.map(flingEntity);
T r = sup.get();
con.accept(r);
} }
} }

View file

@ -0,0 +1,50 @@
package net.friedl.fling.service.archive;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;
/**
* Interface for persisting artifacts
*
* @author Armin Friedl <dev@friedl.net>
*/
public interface ArchiveService {
/**
* Retrieve an artifact from the archive
*
* @param id The artifact id
* @return An {@link InputStream} for reading the artifact
*/
InputStream getArtifact(UUID artifactId) throws IOException;
/**
* Retrieve a packaged fling from the archive
*
* @param flingId The fling id
* @return An {@link InputStream} representing the fling and its artifacts
*/
InputStream getFling(UUID flingId) throws IOException;
/**
* Store an artifact
*
* @param artifactStream The artifact to store represented as {@link InputStream}
* @param artifactId The id of the artifact. Must be an existing artifact in the DB. Not null.
*/
void storeArtifact(UUID artifactId, InputStream artifactStream) throws IOException;
/**
* Delete an artifact
*
* @param id The unique artifact id
*/
void deleteArtifact(UUID artifactId) throws IOException;
/**
* Delete a fling
*
* @param flingId The unique fling id
*/
void deleteFling(UUID flingId) throws IOException;
}

View file

@ -0,0 +1,223 @@
package net.friedl.fling.service.archive.impl;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.transaction.Transactional;
import javax.validation.constraints.NotBlank;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import net.friedl.fling.persistence.entities.ArtifactEntity;
import net.friedl.fling.persistence.repositories.ArtifactRepository;
import net.friedl.fling.service.archive.ArchiveService;
@Slf4j
@Service
@ConfigurationProperties("fling.archive.filesystem")
@Transactional
public class FileSystemArchive implements ArchiveService {
@NotBlank
private Path archivePath;
private ArtifactRepository artifactRepository;
private Map<URI, FileSystem> filesystems;
public FileSystemArchive(ArtifactRepository artifactRepository) {
this.artifactRepository = artifactRepository;
this.filesystems = new HashMap<>();
}
@PostConstruct
public void postConstruct() {
try {
Files.createDirectories(archivePath);
log.debug("Using archive path {}", archivePath);
} catch (IOException e) {
log.error("Could not create directory at archive path {}", archivePath);
throw new UncheckedIOException(e);
}
}
@PreDestroy
public void preDestroy() {
filesystems.forEach((uri, zfs) -> {
try {
zfs.close();
log.debug("Closed {}", uri);
} catch (IOException e) {
log.error("Could not close file system for {}", uri);
}
});
}
@Override
public InputStream getArtifact(UUID artifactId) throws IOException {
log.debug("Reading data for artifact {}", artifactId);
FileSystem zipDisk = getZipDisk(artifactId);
return zipDisk.provider().newInputStream(getZipDiskPath(artifactId, zipDisk),
StandardOpenOption.READ);
// do not close zip disk here or the input stream will be closed as well
}
@Override
public InputStream getFling(UUID flingId) throws IOException {
log.debug("Reading data for fling {}", flingId);
Path zipDiskPath = archivePath.resolve(flingId.toString() + ".zip");
log.debug("Zip disk path is {}", zipDiskPath);
return new FileInputStream(zipDiskPath.toFile());
}
@Override
public void storeArtifact(UUID artifactId, InputStream artifactStream) throws IOException {
log.debug("Storing artifact {}", artifactId);
synchronized (filesystems) {
setArchived(artifactId, false);
FileSystem zipDisk = getZipDisk(artifactId);
Files.copy(artifactStream, getZipDiskPath(artifactId, zipDisk),
StandardCopyOption.REPLACE_EXISTING);
// we need to close the zipDisk in order to flush it to disk
closeZipDisk(artifactId);
setArchived(artifactId, true);
}
}
@Override
public void deleteArtifact(UUID artifactId) throws IOException {
log.debug("Deleting artifact {}", artifactId);
FileSystem zipDisk = getZipDisk(artifactId);
Files.delete(getZipDiskPath(artifactId, zipDisk));
// we need to close the zipDisk in order to flush it to disk
closeZipDisk(artifactId);
setArchived(artifactId, false);
}
@Override
public void deleteFling(UUID flingId) throws IOException {
URI zipDiskUri = resolveFlingUri(flingId);
log.debug("Closing zip disk at {}", zipDiskUri);
// make sure nobody opens the filesystem while it is being closed and deleted
synchronized (filesystems) {
FileSystem zipDisk = filesystems.remove(zipDiskUri);
if (zipDisk != null) {
zipDisk.close();
log.debug("Zip disk closed");
} else {
log.debug("No open zip disk found");
}
Path zipDiskPath = archivePath.resolve(flingId.toString() + ".zip");
log.debug("Deleting fling [.id={}] at {}", flingId, zipDiskPath);
if (Files.exists(zipDiskPath)) {
Files.delete(zipDiskPath);
} else {
log.warn("No fling disk found at {}", zipDiskPath);
}
artifactRepository.findAllByFlingId(flingId).forEach(ar -> ar.setArchived(false));
}
}
private void setArchived(UUID artifactId, boolean archived) {
ArtifactEntity artifactEntity = artifactRepository.getOne(artifactId);
artifactEntity.setArchived(archived);
log.debug("Artifact[.id={}] set to {} archived", artifactId, archived ? "" : "not");
}
private Path getZipDiskPath(UUID artifactId, FileSystem zipDisk) {
ArtifactEntity artifactEntity = artifactRepository.getOne(artifactId);
log.debug("Getting zip disk path for {}", artifactEntity.getPath());
Path zipDiskPath = zipDisk.getPath(artifactEntity.getPath().toString());
if (zipDiskPath.getParent() != null && !Files.exists(zipDiskPath.getParent())) {
try {
Files.createDirectories(zipDiskPath.getParent());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
log.debug("Got zip disk path {}", zipDiskPath);
return zipDiskPath;
}
private FileSystem getZipDisk(UUID artifactId) throws IOException {
log.debug("Retrieving zip disk for artifact {}", artifactId);
URI uri = resolveArtifactUri(artifactId);
log.debug("Looking for zip disk at uri {}", uri);
// make sure nobody closes, deletes or interleavingly opens the filesystem while it is
// being opened
synchronized (filesystems) {
if (!filesystems.containsKey(uri)) {
log.debug("Zip disk does not exist. Creating zip disk for {}", uri);
FileSystem zipDisk = FileSystems.newFileSystem(uri, Map.of("create", "true"));
filesystems.put(uri, zipDisk);
}
return filesystems.get(uri);
}
}
private void closeZipDisk(UUID artifactId) throws IOException {
log.debug("Closing zip disk for artifact {}", artifactId);
URI uri = resolveArtifactUri(artifactId);
log.debug("Closing zip disk at uri {}", uri);
// make sure nobody opens the filesystem while it is being closed
synchronized (filesystems) {
FileSystem zipDisk = filesystems.remove(uri);
if (zipDisk == null) {
log.warn("Could not close zip disk at {}. Filesystem not found.", uri);
return;
}
zipDisk.close();
}
}
private URI resolveArtifactUri(UUID artifactId) throws IOException {
ArtifactEntity artifactEntity = artifactRepository.getOne(artifactId);
UUID flingId = artifactEntity.getFling().getId();
return resolveFlingUri(flingId);
}
private URI resolveFlingUri(UUID flingId) throws IOException {
Path zipDiskPath = archivePath.resolve(flingId.toString() + ".zip");
return URI.create("jar:file:" + zipDiskPath.toFile().getCanonicalPath());
}
public void setArchivePath(String archivePath) {
this.archivePath = Paths.get(archivePath);
}
public void setArchivePath(Path archivePath) {
this.archivePath = archivePath;
}
}

View file

@ -0,0 +1,75 @@
{
"groups": [
{
"name": "fling.archive.filesystem",
"type": "net.friedl.fling.service.archive.impl.FileSystemArchive",
"sourceType": "net.friedl.fling.service.archive.impl.FileSystemArchive"
},
{
"name": "fling.security",
"type": "net.friedl.fling.security.FlingWebSecurityConfiguration",
"sourceType": "net.friedl.fling.security.FlingWebSecurityConfiguration"
},
{
"name": "fling.api",
"type": "net.friedl.fling.controller.OpenApiConfiguration",
"sourceType": "net.friedl.fling.controller.OpenApiConfiguration"
}
],
"properties": [
{
"name": "fling.api.version",
"type": "java.lang.String",
"description": "Fling API version",
"sourceType": "net.friedl.fling.controller.OpenApiConfiguration"
},
{
"name": "fling.api.server-url",
"type": "java.lang.String",
"description": "Base URL for the fling api",
"sourceType": "net.friedl.fling.controller.OpenApiConfiguration"
},
{
"name": "fling.api.server-description",
"type": "java.lang.String",
"description": "A description for the server to be shown in OAS",
"sourceType": "net.friedl.fling.controller.OpenApiConfiguration"
},
{
"name": "fling.archive.filesystem.archive-path",
"type": "java.lang.String",
"description": "Directory where FileSystemArchive stores its data",
"sourceType": "net.friedl.fling.service.archive.impl.FileSystemArchive"
},
{
"name": "fling.security.allowed-origins",
"type": "java.util.List",
"description": "Allowed origins for CORS",
"sourceType": "net.friedl.fling.security.FlingWebSecurityConfiguration"
},
{
"name": "fling.security.admin-name",
"type": "java.lang.String",
"description": "Username of the admin user/instance owner",
"sourceType": "net.friedl.fling.security.FlingWebSecurityConfiguration"
},
{
"name": "fling.security.admin-password",
"type": "java.util.String",
"description": "Password of the admin user/instance owner",
"sourceType": "net.friedl.fling.security.FlingWebSecurityConfiguration"
},
{
"name": "fling.security.signing-key",
"type": "java.util.String",
"description": "Key for signing JWT tokens. Must be 256 bits (32 bytes)",
"sourceType": "net.friedl.fling.security.FlingWebSecurityConfiguration"
},
{
"name": "fling.security.jwt-expiration",
"type": "java.util.Long",
"description": "Time until JWT tokens expire",
"sourceType": "net.friedl.fling.security.FlingWebSecurityConfiguration"
}
]
}

View file

@ -21,14 +21,18 @@ logging.level:
# spring.http.log-request-details: true # spring.http.log-request-details: true
fling: fling:
archive.fileystem.directory: "/home/armin/Desktop/fling" archive.filesystem.archive-path: /home/armin/Desktop/fling
security: security:
allowed-origins: allowed-origins:
- "https://friedl.net" - "https://friedl.net"
- "http://localhost:3000" - "http://localhost:3000"
- "http://localhost:5000" - "http://localhost:5000"
- "http://10.0.2.2:5000" - "http://10.0.2.2:5000"
admin-user: "${FLING_ADMIN_USER:admin}" admin-name: "admin"
admin-password: "${FLING_ADMIN_PASSWORD:123}" admin-password: "123"
signing-key: "${FLING_SIGNING_KEY:changeitchangeitchangeitchangeit}" signing-key: "changeitchangeitchangeitchangeit"
jwt-expiration: "${FLING_JWT_EXPIRATION:180000}" jwt-expiration: "180000"
api:
version: "0.1.0-snapshot"
server-url: "http://localhost:8080"
server-description: "API server for dev"

View file

@ -16,12 +16,18 @@ logging.level:
root: WARN root: WARN
fling: fling:
archive.fileystem.directory: "/var/fling/files" archive.filesystem.archive-path: "/var/fling/files"
security: security:
allowed-origins: allowed-origins:
- "https://fling.friedl.net" - "https://friedl.net"
- "http://localhost:3000" - "http://localhost:3000"
admin-user: "${FLING_ADMIN_USER:admin}" - "http://localhost:5000"
admin-password: "${FLING_ADMIN_PASSWORD:123}" - "http://10.0.2.2:5000"
signing-key: "${FLING_SIGNING_KEY:changeitchangeitchangeitchangeit}" admin-name: "adminName"
jwt-expiration: "${FLING_JWT_EXPIRATION:180000}" admin-password: "adminPassword"
signing-key: "changeitchangeitchangeitchangeit"
jwt-expiration: "180000"
api:
version: "0.1.0-snapshot"
server-url: "http://localhost:8080"
server-description: "API server for dev"

View file

@ -1 +1,3 @@
spring.profiles.active: "@spring.profiles.active@" # To be replaced by maven according to profile settings # To be replaced by maven
spring.profiles.active: "@spring.profiles.active@"
fling.api.version: "@fling.api.version@"

View file

@ -1,11 +1,24 @@
package net.friedl.fling.controller; package net.friedl.fling.controller;
import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.equalTo;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import java.util.List; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Instant;
import java.util.UUID;
import javax.persistence.EntityNotFoundException;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
@ -13,9 +26,12 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType; import org.springframework.context.annotation.FilterType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import net.friedl.fling.model.dto.ArtifactDto; import net.friedl.fling.model.dto.ArtifactDto;
import net.friedl.fling.service.ArtifactService; import net.friedl.fling.service.ArtifactService;
import net.friedl.fling.service.archive.ArchiveService;
@WebMvcTest(controllers = ArtifactController.class, @WebMvcTest(controllers = ArtifactController.class,
// do auto-configure security // do auto-configure security
@ -29,28 +45,98 @@ class ArtifactControllerTest {
@MockBean @MockBean
private ArtifactService artifactService; private ArtifactService artifactService;
@MockBean
private ArchiveService archiveService;
private static final UUID ARTIFACT_ID = UUID.randomUUID();
private ArtifactDto artifactDto =
new ArtifactDto(ARTIFACT_ID, Path.of("testArtifact"), Instant.EPOCH, false);
@Test @Test
public void testGetArtifacts_noArtifacts_empty() throws Exception { public void getArtifact_noArtifactWithId_notFound() throws Exception {
Long flingId = 123L; when(artifactService.getById(ARTIFACT_ID)).thenThrow(EntityNotFoundException.class);
when(artifactService.findAllArtifacts(flingId)).thenReturn(List.of()); mvc.perform(get("/api/artifacts/{id}", ARTIFACT_ID))
.andExpect(status().isNotFound());
mvc.perform(get("/api/artifacts").param("flingId", flingId.toString()))
.andExpect(jsonPath("$", hasSize(0)));
} }
@Test @Test
public void testGetArtifacts_hasArtifacts_allArtifacts() throws Exception { public void getArtifacts_ok() throws Exception {
Long flingId = 123L; when(artifactService.getById(ARTIFACT_ID)).thenReturn(artifactDto);
String artifactName = "TEST";
ArtifactDto artifactDto = new ArtifactDto(); mvc.perform(get("/api/artifacts/{id}", ARTIFACT_ID))
artifactDto.setName(artifactName); .andExpect(status().isOk())
.andExpect(jsonPath("$.id", equalTo(ARTIFACT_ID.toString())));
}
when(artifactService.findAllArtifacts(flingId)).thenReturn(List.of(artifactDto)); @Test
public void deleteArtifact_noArtifactWithId_notFound() throws Exception {
doThrow(EntityNotFoundException.class).when(artifactService).delete(ARTIFACT_ID);
mvc.perform(get("/api/artifacts").param("flingId", flingId.toString())) mvc.perform(delete("/api/artifacts/{id}", ARTIFACT_ID))
.andExpect(jsonPath("$", hasSize(1))) .andExpect(status().isNotFound());
.andExpect(jsonPath("$[0].name", equalTo(artifactName))); }
@Test
public void deleteArtifact_ok() throws Exception {
mvc.perform(delete("/api/artifacts/{id}", ARTIFACT_ID))
.andExpect(status().isOk());
}
@Test
public void uploadArtifact_ioError_serverError() throws Exception {
doThrow(IOException.class).when(archiveService).storeArtifact(any(), any());
byte[] payload = "Payload".getBytes();
mvc.perform(post("/api/artifacts/{id}/data", ARTIFACT_ID)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.content(payload))
.andExpect(status().isInternalServerError());
}
@Test
public void uploadArtifact_ok() throws Exception {
byte[] payload = "Payload".getBytes();
mvc.perform(post("/api/artifacts/{id}/data", ARTIFACT_ID)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.content(payload))
.andExpect(status().isOk());
}
@Test
public void downloadArtifact_noArtifact_notFound() throws Exception {
doThrow(EntityNotFoundException.class).when(artifactService).getById(ARTIFACT_ID);
mvc.perform(get("/api/artifacts/{id}/data", ARTIFACT_ID))
.andExpect(header().doesNotExist(HttpHeaders.CONTENT_DISPOSITION))
.andExpect(header().string(HttpHeaders.CONTENT_TYPE,
not(equalTo(MediaType.APPLICATION_OCTET_STREAM_VALUE))))
.andExpect(status().isNotFound());
}
@Test
public void downloadArtifact_ioError_serverError() throws Exception {
doThrow(IOException.class).when(archiveService).getArtifact(ARTIFACT_ID);
mvc.perform(get("/api/artifacts/{id}/data", ARTIFACT_ID))
.andExpect(header().doesNotExist(HttpHeaders.CONTENT_DISPOSITION))
.andExpect(header().string(HttpHeaders.CONTENT_TYPE,
not(equalTo(MediaType.APPLICATION_OCTET_STREAM_VALUE))))
.andExpect(status().isInternalServerError());
}
@Test
public void downloadArtifact_ok() throws Exception {
when(artifactService.getById(ARTIFACT_ID)).thenReturn(artifactDto);
byte[] testData = "test".getBytes();
when(archiveService.getArtifact(any())).thenReturn(new ByteArrayInputStream(testData));
mvc.perform(get("/api/artifacts/{id}/data", ARTIFACT_ID))
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
.andExpect(header().exists(HttpHeaders.CONTENT_DISPOSITION))
.andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION,
Matchers.containsString("attachment;filename")))
.andExpect(content().bytes(testData));
} }
} }

View file

@ -0,0 +1,88 @@
package net.friedl.fling.controller;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.friedl.fling.model.dto.AdminAuthDto;
import net.friedl.fling.model.dto.UserAuthDto;
import net.friedl.fling.service.AuthenticationService;
import net.friedl.fling.service.AuthorizationService;
@WebMvcTest(controllers = AuthenticationController.class,
includeFilters = {@Filter(Configuration.class)})
@ActiveProfiles("local")
public class AuthenticationControllerTest {
@Autowired
private MockMvc mvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private AuthenticationService authenticationService;
@MockBean
private AuthorizationService authorizationService;
@Test
public void authenticateOwner_noToken_403() throws Exception {
AdminAuthDto adminAuthDto = new AdminAuthDto("admin", "123");
when(authenticationService.authenticate(any(AdminAuthDto.class))).thenReturn(Optional.empty());
mvc.perform(post("/api/auth/admin")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(adminAuthDto)))
.andExpect(status().is(403));
}
@Test
public void authenticateOwner_token_ok() throws Exception {
AdminAuthDto adminAuthDto = new AdminAuthDto("admin", "123");
when(authenticationService.authenticate(any(AdminAuthDto.class)))
.thenReturn(Optional.of("token"));
mvc.perform(post("/api/auth/admin")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(adminAuthDto)))
.andExpect(status().is(200))
.andExpect(content().string("token"));
}
@Test
public void authenticateUser_noToken_403() throws Exception {
UserAuthDto userAuthDto = new UserAuthDto("shareId", "authCode");
when(authenticationService.authenticate(any(UserAuthDto.class))).thenReturn(Optional.empty());
mvc.perform(post("/api/auth/user")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(userAuthDto)))
.andExpect(status().is(403));
}
@Test
public void authenticateUser_token_ok() throws Exception {
UserAuthDto userAuthDto = new UserAuthDto("shareId", "authCode");
when(authenticationService.authenticate(any(UserAuthDto.class)))
.thenReturn(Optional.of("token"));
mvc.perform(post("/api/auth/user")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(userAuthDto)))
.andExpect(status().is(200))
.andExpect(content().string("token"));
}
}

View file

@ -0,0 +1,291 @@
package net.friedl.fling.controller;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.not;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Instant;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import javax.persistence.EntityNotFoundException;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.friedl.fling.model.dto.ArtifactDto;
import net.friedl.fling.model.dto.FlingDto;
import net.friedl.fling.service.ArtifactService;
import net.friedl.fling.service.FlingService;
import net.friedl.fling.service.archive.ArchiveService;
@WebMvcTest(controllers = FlingController.class,
// do auto-configure security
excludeAutoConfiguration = SecurityAutoConfiguration.class,
// do not try to create beans in security
excludeFilters = @Filter(type = FilterType.REGEX, pattern = "net.friedl.fling.security.*"))
public class FlingControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper mapper;
@MockBean
private FlingService flingService;
@MockBean
private ArtifactService artifactService;
@MockBean
private ArchiveService archiveService;
private static final UUID flingId = UUID.randomUUID();
private FlingDto flingDto = new FlingDto(flingId, "name", Instant.EPOCH, "shareId", "authCode",
false, true, true, 1, null);
private ArtifactDto artifactDto =
new ArtifactDto(UUID.randomUUID(), Path.of("testArtifact"), Instant.EPOCH, false);
@Test
public void getFlings_noFlings_empty() throws Exception {
when(flingService.findAll()).thenReturn(List.of());
mockMvc.perform(get("/api/fling"))
.andExpect(jsonPath("$", hasSize(0)))
.andExpect(status().isOk());
}
@Test
public void getFlings_allFlings() throws Exception {
when(flingService.findAll()).thenReturn(List.of(flingDto, flingDto));
mockMvc.perform(get("/api/fling"))
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].id", equalTo(flingId.toString())))
.andExpect(status().isOk());
}
@Test
public void postFling_ok() throws Exception {
mockMvc.perform(post("/api/fling")
.content(mapper.writeValueAsString(flingDto))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
@Test
public void postFling_validatesBody_notOk() throws Exception {
FlingDto invalidFlingDto = new FlingDto();
mockMvc.perform(post("/api/fling")
.content(mapper.writeValueAsString(invalidFlingDto))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest());
}
@Test
public void replaceFling_validatesBody_notOk() throws Exception {
FlingDto invalidFlingDto = new FlingDto();
mockMvc.perform(put("/api/fling/{id}", flingId)
.content(mapper.writeValueAsString(invalidFlingDto))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest());
}
@Test
public void replaceFling_ok() throws Exception {
FlingDto flingDto = new FlingDto(flingId, "new-name", Instant.EPOCH, "shareId", "new-authCode",
false, true, true, 1, null);
mockMvc.perform(put("/api/fling/{id}", flingId)
.content(mapper.writeValueAsString(flingDto))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
@Test
public void postArtifact_ok() throws Exception {
mockMvc.perform(post("/api/fling/{id}/artifacts", flingId)
.content(mapper.writeValueAsString(artifactDto))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
@Test
public void postArtifact_validatesBody_notOk() throws Exception {
ArtifactDto invalidArtifactDto = new ArtifactDto();
mockMvc.perform(post("/api/fling/{id}/artifacts", flingId)
.content(mapper.writeValueAsString(invalidArtifactDto))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest());
}
@Test
public void getArtifact_noFlingWithId_notFound() throws Exception {
doThrow(EntityNotFoundException.class).when(flingService).getArtifacts(flingId);
mockMvc.perform(get("/api/fling/{id}/artifacts", flingId))
.andExpect(status().isNotFound());
}
@Test
public void getArtifact_flingFound_noArtifacts_emptySet() throws Exception {
when(flingService.getArtifacts(flingId)).thenReturn(Set.of());
mockMvc.perform(get("/api/fling/{id}/artifacts", flingId))
.andExpect(status().isOk())
.andExpect(content().string(equalTo("[]")));
}
@Test
public void getArtifact_flingFound_hasArtifacts_returnArtifacts() throws Exception {
ArtifactDto artifactDto1 = ArtifactDto.builder()
.id(new UUID(0, 0))
.build();
ArtifactDto artifactDto2 = ArtifactDto.builder()
.id(new UUID(0, 1))
.build();
when(flingService.getArtifacts(flingId)).thenReturn(Set.of(artifactDto1, artifactDto2));
mockMvc.perform(get("/api/fling/{id}/artifacts", flingId))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].id",
anyOf(equalTo(new UUID(0, 0).toString()), equalTo(new UUID(0, 1).toString()))))
.andExpect(jsonPath("$[1].id",
anyOf(equalTo(new UUID(0, 0).toString()), equalTo(new UUID(0, 1).toString()))));
}
@Test
public void getFling_noFlingWithId_notFound() throws Exception {
doThrow(EntityNotFoundException.class).when(flingService).getById(flingId);
mockMvc.perform(get("/api/fling/{id}", flingId))
.andExpect(status().isNotFound());
}
@Test
public void getFling_flingFound_returnsFling() throws Exception {
when(flingService.getById(flingId)).thenReturn(flingDto);
mockMvc.perform(get("/api/fling/{id}", flingId))
.andExpect(jsonPath("$.id", equalTo(flingId.toString())))
.andExpect(status().isOk());
}
@Test
public void getFlingByShareId_noFlingWithShareId_notFound() throws Exception {
doThrow(EntityNotFoundException.class).when(flingService).getByShareId("doesNotExist");
mockMvc.perform(get("/api/fling/share/{shareId}", "doesNotExist"))
.andExpect(status().isNotFound());
}
@Test
public void getFlingByShareId_flingFind_returnsFling() throws Exception {
doReturn(flingDto).when(flingService).getByShareId("shareId");
mockMvc.perform(get("/api/fling/share/{shareId}", "shareId"))
.andExpect(jsonPath("$.id", equalTo(flingId.toString())))
.andExpect(status().isOk());
}
@Test
public void deleteFling_noFlingWithId_notFound() throws Exception {
doThrow(EntityNotFoundException.class).when(flingService).delete(flingId);
mockMvc.perform(delete("/api/fling/{id}", flingId))
.andExpect(status().isNotFound());
}
@Test
public void deleteFling_ok() throws Exception {
doNothing().when(flingService).delete(flingId);
mockMvc.perform(delete("/api/fling/{id}", flingId))
.andExpect(status().isOk());
}
@Test
public void getFlingData_ioError_serverError() throws Exception {
doThrow(IOException.class).when(archiveService).getFling(flingId);
mockMvc.perform(get("/api/fling/{id}/data", flingId))
.andExpect(header().doesNotExist(HttpHeaders.CONTENT_DISPOSITION))
.andExpect(header().string(HttpHeaders.CONTENT_TYPE,
not(equalTo(MediaType.APPLICATION_OCTET_STREAM_VALUE))))
.andExpect(status().isInternalServerError());
}
@Test
public void getFlingData_ok() throws Exception {
when(flingService.getById(flingId)).thenReturn(flingDto);
int[] testZipInt = new int[] {
0x50, 0x4b, 0x03, 0x04, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0x77, 0xe4, 0x50, 0xc6,
0x35,
0xb9, 0x3b, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x74,
0x65,
0x73, 0x74, 0x55, 0x54, 0x09, 0x00, 0x03, 0x40, 0x7d, 0x00, 0x5f, 0x37, 0x7d, 0x00, 0x5f,
0x75,
0x78, 0x0b, 0x00, 0x01, 0x04, 0xe8, 0x03, 0x00, 0x00, 0x04, 0xe8, 0x03, 0x00, 0x00, 0x74,
0x65,
0x73, 0x74, 0x0a, 0x50, 0x4b, 0x01, 0x02, 0x1e, 0x03, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00,
0x76,
0x77, 0xe4, 0x50, 0xc6, 0x35, 0xb9, 0x3b, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00,
0x04,
0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xb4, 0x81, 0x00, 0x00,
0x00,
0x00, 0x74, 0x65, 0x73, 0x74, 0x55, 0x54, 0x05, 0x00, 0x03, 0x40, 0x7d, 0x00, 0x5f, 0x75,
0x78,
0x0b, 0x00, 0x01, 0x04, 0xe8, 0x03, 0x00, 0x00, 0x04, 0xe8, 0x03, 0x00, 0x00, 0x50, 0x4b,
0x05,
0x06, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x43, 0x00,
0x00,
0x00, 0x00, 0x00
};
byte[] testZip = new byte[testZipInt.length];
for (int idx = 0; idx < testZip.length; idx++) testZip[idx] = (byte) testZipInt[idx];
when(archiveService.getFling(any()))
.thenAnswer((invocation) -> {
// need to use thenAnswer here to always return a fresh new (unclosed) input stream
return new ByteArrayInputStream(testZip);
});
mockMvc.perform(get("/api/fling/{id}/data", flingId))
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
.andExpect(header().exists(HttpHeaders.CONTENT_DISPOSITION))
.andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION,
Matchers.containsString("attachment;filename")))
.andExpect(content().bytes(testZip));
}
}

View file

@ -0,0 +1,64 @@
package net.friedl.fling.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Paths;
import java.util.Set;
import java.util.UUID;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import net.friedl.fling.model.dto.ArtifactDto;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = ModelTestConfiguration.class)
public class ArtifactDtoTest {
@Autowired
private Validator validator;
@Test
void testSetId_null_validationOk() {
ArtifactDto artifactDto = ArtifactDto.builder()
.id(null)
.path(Paths.get("test"))
.build();
Set<ConstraintViolation<ArtifactDto>> constraintViolations = validator.validate(artifactDto);
assertThat(constraintViolations).hasSize(0);
}
@Test
void testSetPath_null_validationFails() {
ArtifactDto artifactDto = ArtifactDto.builder()
.id(new UUID(0L, 0L))
.path(null)
.build();
Set<ConstraintViolation<ArtifactDto>> constraintViolations = validator.validate(artifactDto);
assertThat(constraintViolations).hasSize(1);
ConstraintViolation<ArtifactDto> violation = constraintViolations.iterator().next();
assertThat(violation.getPropertyPath().toString()).isEqualTo("path");
assertThat(violation.getMessage()).isEqualTo("must not be null");
}
@Test
void testMandatoryFieldsSet_validationOk() {
ArtifactDto artifactDto = ArtifactDto.builder()
.id(new UUID(0L, 0L))
.path(Paths.get("test"))
.build();
Set<ConstraintViolation<ArtifactDto>> constraintViolations = validator.validate(artifactDto);
assertTrue(constraintViolations.isEmpty());
}
}

View file

@ -0,0 +1,101 @@
package net.friedl.fling.model;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.Instant;
import java.util.Set;
import java.util.UUID;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import net.friedl.fling.model.dto.FlingDto;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = ModelTestConfiguration.class)
public class FlingDtoTest {
@Autowired
private Validator validator;
@Test
void testSetId_null_validationOk() {
FlingDto flingDto = FlingDto.builder()
.id(null)
.name("test")
.creationTime(Instant.EPOCH)
.shareId("test")
.build();
Set<ConstraintViolation<FlingDto>> constraintViolations = validator.validate(flingDto);
assertThat(constraintViolations).hasSize(0);
}
@Test
void testSetName_null_validationFails() {
FlingDto flingDto = FlingDto.builder()
.id(new UUID(0L, 0L))
.name(null)
.creationTime(Instant.EPOCH)
.shareId("test")
.build();
Set<ConstraintViolation<FlingDto>> constraintViolations = validator.validate(flingDto);
assertThat(constraintViolations).hasSize(1);
ConstraintViolation<FlingDto> violation = constraintViolations.iterator().next();
assertThat(violation.getPropertyPath().toString()).isEqualTo("name");
assertThat(violation.getMessage()).isEqualTo("must not be null");
}
@Test
void testSetCreationTime_null_validationOk() {
FlingDto flingDto = FlingDto.builder()
.id(new UUID(0L, 0L))
.name("test")
.creationTime(null)
.shareId("test")
.build();
Set<ConstraintViolation<FlingDto>> constraintViolations = validator.validate(flingDto);
assertThat(constraintViolations).hasSize(0);
}
@Test
void testSetShareId_null_validationOk() { // must be nullable to support defaulting in service
FlingDto flingDto = FlingDto.builder()
.id(new UUID(0L, 0L))
.name("test")
.creationTime(Instant.EPOCH)
.shareId(null)
.build();
Set<ConstraintViolation<FlingDto>> constraintViolations = validator.validate(flingDto);
assertThat(constraintViolations).hasSize(0);
}
@Test
void testSetAllManadatory_validationOk() {
FlingDto flingDto = FlingDto.builder()
.id(new UUID(0L, 0L))
.name("test")
.creationTime(Instant.EPOCH)
.shareId("test")
.build();
Set<ConstraintViolation<FlingDto>> constraintViolations = validator.validate(flingDto);
assertTrue(constraintViolations.isEmpty());
}
}

View file

@ -0,0 +1,17 @@
package net.friedl.fling.model;
import javax.validation.Validator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@Configuration
public class ModelTestConfiguration {
@Bean
public Validator validator() {
LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
localValidatorFactoryBean.afterPropertiesSet();
return localValidatorFactoryBean;
}
}

View file

@ -0,0 +1,130 @@
package net.friedl.fling.service;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.nio.file.Path;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import net.friedl.fling.model.dto.ArtifactDto;
import net.friedl.fling.model.mapper.ArtifactMapper;
import net.friedl.fling.model.mapper.ArtifactMapperImpl;
import net.friedl.fling.persistence.entities.ArtifactEntity;
import net.friedl.fling.persistence.entities.FlingEntity;
import net.friedl.fling.persistence.repositories.ArtifactRepository;
import net.friedl.fling.persistence.repositories.FlingRepository;
import net.friedl.fling.service.archive.ArchiveService;
@ExtendWith(SpringExtension.class)
public class ArtifactServiceTest {
@Autowired
private ArtifactService artifactService;
@MockBean
private FlingRepository flingRepository;
@MockBean
private ArtifactRepository artifactRepository;
@MockBean
private ArchiveService archiveService;
private FlingEntity flingEntity;
private ArtifactEntity artifactEntity1;
private ArtifactEntity artifactEntity2;
@TestConfiguration
static class FlingServiceTestConfiguration {
@Bean
public ArtifactMapper artifactMapper() {
return new ArtifactMapperImpl();
}
@Bean
public ArtifactService artifactService(ArtifactRepository artifactRepository,
FlingRepository flingRepository, ArtifactMapper artifactMapper,
ArchiveService archiveService) {
return new ArtifactService(artifactRepository, flingRepository, artifactMapper,
archiveService);
}
}
@BeforeEach
public void beforeEach() {
this.artifactEntity1 = new ArtifactEntity();
artifactEntity1.setId(UUID.randomUUID());
artifactEntity1.setPath(Path.of("artifact1"));
this.artifactEntity2 = new ArtifactEntity();
artifactEntity2.setId(UUID.randomUUID());
artifactEntity2.setPath(Path.of("/", "/sub", "artifact2"));
this.flingEntity = new FlingEntity();
flingEntity.setId(UUID.randomUUID());
flingEntity.setName("fling");
when(flingRepository.save(any())).then(new Answer<FlingEntity>() {
@Override
public FlingEntity answer(InvocationOnMock invocation) throws Throwable {
FlingEntity flingEntity = invocation.getArgument(0);
if (flingEntity.getId() == null) flingEntity.setId(UUID.randomUUID());
return flingEntity;
}
});
when(artifactRepository.save(any())).then(new Answer<ArtifactEntity>() {
@Override
public ArtifactEntity answer(InvocationOnMock invocation) throws Throwable {
ArtifactEntity artifactEntity = invocation.getArgument(0);
artifactEntity.setId(UUID.randomUUID());
return artifactEntity;
}
});
}
@Test
public void getById_artifactExists_ok() {
when(artifactRepository.getOne(artifactEntity1.getId())).thenReturn(artifactEntity1);
ArtifactDto artifactDto = artifactService.getById(artifactEntity1.getId());
assertThat(artifactDto.getId(), equalTo(artifactEntity1.getId()));
assertThat(artifactDto.getPath(), equalTo(artifactEntity1.getPath()));
}
@Test
public void create_createsArtifact_ok() {
ArtifactDto artifactToCreate = ArtifactDto.builder()
.path(Path.of("new", "artifacts"))
.build();
ArtifactDto createdArtifact = artifactService.create(flingEntity.getId(), artifactToCreate);
assertThat(createdArtifact.getPath(), equalTo(artifactToCreate.getPath()));
}
@Test
public void delete_deletesArchiveAndArtifactEntry() throws IOException {
artifactService.delete(artifactEntity1.getId());
verify(archiveService).deleteArtifact(artifactEntity1.getId());
verify(artifactRepository).deleteById(artifactEntity1.getId());
}
}

View file

@ -0,0 +1,300 @@
package net.friedl.fling.service;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.emptyOrNullString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.security.Key;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import javax.persistence.EntityNotFoundException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.impl.DefaultClaims;
import io.jsonwebtoken.impl.DefaultJws;
import io.jsonwebtoken.impl.DefaultJwsHeader;
import io.jsonwebtoken.security.Keys;
import net.friedl.fling.model.dto.AdminAuthDto;
import net.friedl.fling.model.dto.UserAuthDto;
import net.friedl.fling.persistence.entities.FlingEntity;
import net.friedl.fling.persistence.entities.TokenEntity;
import net.friedl.fling.persistence.repositories.FlingRepository;
import net.friedl.fling.persistence.repositories.TokenRepository;
import net.friedl.fling.security.FlingAuthorities;
import net.friedl.fling.security.authentication.FlingToken;
import net.friedl.fling.security.authentication.authorities.FlingAdminAuthority;
import net.friedl.fling.security.authentication.authorities.FlingUserAuthority;
@ExtendWith(SpringExtension.class)
@TestPropertySource("classpath:/application-test.properties")
@ActiveProfiles("test")
public class AuthenticationServiceTest {
@Autowired
public AuthenticationService authenticationService;
@MockBean
private FlingRepository flingRepository;
@MockBean
private TokenRepository tokenRepository;
@MockBean
private JwtParser jwtParser;
@MockBean
private PasswordEncoder passwordEncoder;
@TestConfiguration
static class FlingServiceTestConfiguration {
private Key jwtSigningKey = Keys.hmacShaKeyFor(new byte[32]);
@Bean
public AuthenticationService authenticationService(JwtParser jwtParser,
PasswordEncoder passwordEncoder, FlingRepository flingRepository,
TokenRepository tokenRepository) {
return new AuthenticationService(jwtParser, jwtSigningKey, passwordEncoder, flingRepository,
tokenRepository);
}
}
@Test
public void authenticate_adminNameDiffers_empty() {
AdminAuthDto adminAuthDto = new AdminAuthDto("wrongadmin", "123");
assertThat(authenticationService.authenticate(adminAuthDto), equalTo(Optional.empty()));
}
@Test
public void authenticate_passwordDiffers_empty() {
AdminAuthDto adminAuthDto = new AdminAuthDto("admin", "wrongpassword");
assertThat(authenticationService.authenticate(adminAuthDto), equalTo(Optional.empty()));
}
@Test
public void authenticate_ok() {
AdminAuthDto adminAuthDto = new AdminAuthDto("admin", "123");
assertThat(authenticationService.authenticate(adminAuthDto), not(equalTo(Optional.empty())));
}
@Test
public void authenticate_authCodeDiffers_empty() {
FlingEntity flingEntity = new FlingEntity();
flingEntity.setAuthCode("test");
flingEntity.setId(UUID.randomUUID());
UserAuthDto userAuthDto = new UserAuthDto("shareId", "wrongCode");
when(flingRepository.findByShareId(any(String.class))).thenReturn(flingEntity);
when(passwordEncoder.encode(any(String.class))).thenReturn("wrongCode");
assertThat(authenticationService.authenticate(userAuthDto), equalTo(Optional.empty()));
}
@Test
public void authenticate_authCodeEmpty_ok() {
FlingEntity flingEntity = new FlingEntity();
UserAuthDto userAuthDto = new UserAuthDto("shareId", "");
when(flingRepository.findByShareId(any(String.class))).thenReturn(flingEntity);
assertThat(authenticationService.authenticate(userAuthDto), not(equalTo(Optional.empty())));
}
@Test
public void authenticate_authCodeEquals_ok() {
FlingEntity flingEntity = new FlingEntity();
flingEntity.setAuthCode("authCodeHash");
flingEntity.setId(UUID.randomUUID());
UserAuthDto userAuthDto = UserAuthDto.builder()
.authCode("authCode")
.shareId("shareId").build();
when(flingRepository.findByShareId(any(String.class))).thenReturn(flingEntity);
when(passwordEncoder.encode(any(String.class))).thenReturn("authCodeHash");
when(passwordEncoder.matches("authCode", "authCodeHash")).thenReturn(true);
assertThat(authenticationService.authenticate(userAuthDto), not(equalTo(Optional.empty())));
}
@Test
public void authenticate_noFlingForShareId_throws() {
UserAuthDto userAuthDto = UserAuthDto.builder()
.authCode("authCode")
.shareId("doesNotExist").build();
when(flingRepository.findByShareId(any(String.class))).thenReturn(null);
when(passwordEncoder.encode(any(String.class))).thenReturn("authCodeHash");
assertThrows(EntityNotFoundException.class,
() -> authenticationService.authenticate(userAuthDto));
}
@Test
public void parseJwtAuthentication_owner_AdminAuthority() {
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
new DefaultClaims(Map.of("sub", "admin")), "signature");
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
FlingToken flingToken = authenticationService.parseJwtAuthentication("any");
assertThat(flingToken.isAuthenticated(), equalTo(true));
// authorized for any fling
assertThat(flingToken.authorizedForFling(UUID.randomUUID()), equalTo(true));
assertThat(flingToken.getCredentials(), equalTo("any"));
assertThat(flingToken.getAuthorities(),
hasItem(org.hamcrest.Matchers.any(FlingAdminAuthority.class)));
}
@Test
public void parseJwtAuthentication_user_UserAuthorityForId() {
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
new DefaultClaims(Map.of("sub", "user", "id", new UUID(0, 0).toString())), "signature");
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
FlingToken flingToken = authenticationService.parseJwtAuthentication("any");
assertThat(flingToken.isAuthenticated(), equalTo(true));
// authorized for fling in token
assertThat(flingToken.authorizedForFling(new UUID(0, 0)), equalTo(true));
// not authorized for fling other flings
assertThat(flingToken.authorizedForFling(new UUID(0, 1)), equalTo(false));
assertThat(flingToken.getCredentials(), equalTo("any"));
assertThat(flingToken.getAuthorities(),
hasItem(org.hamcrest.Matchers.any(FlingUserAuthority.class)));
}
@Test
public void parseJwtAuthentication_unknownSubject_throws() {
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
new DefaultClaims(Map.of("sub", "unknownSubject")), "signature");
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
assertThrows(BadCredentialsException.class,
() -> authenticationService.parseJwtAuthentication("any"));
}
@Test
public void deriveToken_noAuthenticationInSecurityContext_throws() {
assertThrows(IllegalStateException.class,
() -> authenticationService.deriveToken(false));
}
@Test
public void deriveToken_authenticationInSecurityContext_ok() {
FlingToken flingToken = new FlingToken(List.of(new FlingAdminAuthority()), "token");
SecurityContextHolder.setContext(new SecurityContextImpl(flingToken));
String derivedToken = authenticationService.deriveToken(null);
assertThat(derivedToken, is(not(emptyOrNullString())));
SecurityContextHolder.clearContext();
}
@Test
public void deriveToken_singleUseNotSet_singleUseIsTrue() {
FlingToken flingToken = new FlingToken(List.of(new FlingAdminAuthority()), "token");
SecurityContextHolder.setContext(new SecurityContextImpl(flingToken));
ArgumentCaptor<TokenEntity> tokenEntityCaptor = ArgumentCaptor.forClass(TokenEntity.class);
authenticationService.deriveToken(null);
verify(tokenRepository).save(tokenEntityCaptor.capture());
assertThat(tokenEntityCaptor.getValue().getSingleUse(), is(true));
}
@Test
public void deriveToken_singleUseFalse_singleUseIsFalse() {
FlingToken flingToken = new FlingToken(List.of(new FlingAdminAuthority()), "token");
SecurityContextHolder.setContext(new SecurityContextImpl(flingToken));
ArgumentCaptor<TokenEntity> tokenEntityCaptor = ArgumentCaptor.forClass(TokenEntity.class);
authenticationService.deriveToken(false);
verify(tokenRepository).save(tokenEntityCaptor.capture());
assertThat(tokenEntityCaptor.getValue().getSingleUse(), is(false));
}
@Test
public void parseDerivedToken_singleUse_deletesToken() {
String token = UUID.randomUUID().toString();
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
new DefaultClaims(Map.of("sub", "admin")), "signature");
TokenEntity tokenEntity = new TokenEntity();
tokenEntity.setId(UUID.fromString(token));
tokenEntity.setSingleUse(true);
tokenEntity.setToken("jwtToken");
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
when(tokenRepository.getOne(any(UUID.class))).thenReturn(tokenEntity);
authenticationService.parseDerivedToken(token);
verify(tokenRepository).delete(tokenEntity);
}
@Test
public void parseDerivedToken_singleUseFalse_doesNotDeleteToken() {
String token = UUID.randomUUID().toString();
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
new DefaultClaims(Map.of("sub", "admin")), "signature");
TokenEntity tokenEntity = new TokenEntity();
tokenEntity.setId(UUID.fromString(token));
tokenEntity.setSingleUse(false);
tokenEntity.setToken("jwtToken");
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
when(tokenRepository.getOne(any(UUID.class))).thenReturn(tokenEntity);
authenticationService.parseDerivedToken(token);
verify(tokenRepository, never()).delete(tokenEntity);
}
@Test
public void parseDerivedToken_returnsParentAuthentication() {
String token = UUID.randomUUID().toString();
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
new DefaultClaims(Map.of("sub", "admin")), "signature");
TokenEntity tokenEntity = new TokenEntity();
tokenEntity.setId(UUID.fromString(token));
tokenEntity.setSingleUse(true);
tokenEntity.setToken("jwtToken");
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
when(tokenRepository.getOne(any(UUID.class))).thenReturn(tokenEntity);
FlingToken flingToken = authenticationService.parseDerivedToken(token);
assertEquals(flingToken.getAuthorities().stream().findFirst().get().getAuthority(),
FlingAuthorities.FLING_ADMIN.getAuthority());
}
}

View file

@ -0,0 +1,135 @@
package net.friedl.fling.service;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import java.util.List;
import java.util.UUID;
import javax.persistence.EntityNotFoundException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import net.friedl.fling.persistence.entities.FlingEntity;
import net.friedl.fling.persistence.repositories.FlingRepository;
import net.friedl.fling.security.authentication.FlingToken;
import net.friedl.fling.security.authentication.authorities.FlingAdminAuthority;
import net.friedl.fling.security.authentication.authorities.FlingUserAuthority;
@ExtendWith(SpringExtension.class)
public class AuthorizationServiceTest {
@Autowired
private AuthorizationService authorizationService;
@MockBean
private FlingRepository flingRepository;
@TestConfiguration
static class FlingServiceTestConfiguration {
@Bean
public AuthorizationService authorizationService(FlingRepository flingRepository) {
return new AuthorizationService(flingRepository);
}
}
@Test
public void allowUpload_unknownToken_false() {
var unkownToken = new AnonymousAuthenticationToken("key", "principal",
List.of(new SimpleGrantedAuthority("role")));
assertFalse(authorizationService.allowUpload(UUID.randomUUID(), unkownToken));
}
@Test
public void allowUpload_flingAdmin_true() {
FlingToken flingToken = new FlingToken(List.of(new FlingAdminAuthority()), "jwtToken");
assertTrue(authorizationService.allowUpload(UUID.randomUUID(), flingToken));
}
@Test
public void allowUpload_noAdmin_uploadDisallowed_false() {
FlingEntity flingEntity = new FlingEntity();
flingEntity.setAllowUpload(false);
FlingToken flingToken =
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
when(flingRepository.getOne(new UUID(0, 0))).thenReturn(flingEntity);
assertFalse(authorizationService.allowUpload(new UUID(0, 0), flingToken));
}
@Test
public void allowUpload_noAdmin_uploadAllowed_notAuthorized_false() {
FlingEntity flingEntity = new FlingEntity();
flingEntity.setAllowUpload(true);
FlingToken flingToken =
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
when(flingRepository.getOne(new UUID(1, 1))).thenReturn(flingEntity);
// Token: UUID(0,0), Request: UUID(1,1)
assertFalse(authorizationService.allowUpload(new UUID(1, 1), flingToken));
}
@Test
public void allowUpload_noAdmin_uploadAllowed_authorized_true() {
FlingEntity flingEntity = new FlingEntity();
flingEntity.setAllowUpload(true);
FlingToken flingToken =
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
when(flingRepository.getOne(new UUID(0, 0))).thenReturn(flingEntity);
// Token: UUID(0,0), Request: UUID(0,0)
assertTrue(authorizationService.allowUpload(new UUID(0, 0), flingToken));
}
@Test
public void allowFlingAccess_unknownToken_false() {
var unkownToken = new AnonymousAuthenticationToken("key", "principal",
List.of(new SimpleGrantedAuthority("role")));
assertFalse(authorizationService.allowFlingAccess(UUID.randomUUID(), unkownToken));
}
@Test
public void allowFlingAcess_flingAdmin_true() {
FlingToken flingToken = new FlingToken(List.of(new FlingAdminAuthority()), "jwtToken");
assertTrue(authorizationService.allowFlingAccess(UUID.randomUUID(), flingToken));
}
@Test
public void allowFlingAcess_flingUser_notAuthorizedForId_false() {
FlingToken flingToken =
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
assertFalse(authorizationService.allowFlingAccess(new UUID(1, 1), flingToken));
}
@Test
public void allowFlingAcess_flingUser_authorizedForId_true() {
FlingToken flingToken =
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
assertTrue(authorizationService.allowFlingAccess(new UUID(0, 0), flingToken));
}
@Test
public void allowFlingAccessByShareId_noFlingForShareId_throw() {
FlingToken flingToken =
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
when(flingRepository.findByShareId(any(String.class))).thenReturn(null);
assertThrows(EntityNotFoundException.class,
() -> authorizationService.allowFlingAccessByShareId("doesNotExist", flingToken));
}
}

View file

@ -0,0 +1,263 @@
package net.friedl.fling.service;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.emptyOrNullString;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.persistence.EntityNotFoundException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import net.friedl.fling.model.dto.ArtifactDto;
import net.friedl.fling.model.dto.FlingDto;
import net.friedl.fling.model.mapper.ArtifactMapper;
import net.friedl.fling.model.mapper.ArtifactMapperImpl;
import net.friedl.fling.model.mapper.FlingMapper;
import net.friedl.fling.model.mapper.FlingMapperImpl;
import net.friedl.fling.persistence.entities.ArtifactEntity;
import net.friedl.fling.persistence.entities.FlingEntity;
import net.friedl.fling.persistence.repositories.FlingRepository;
import net.friedl.fling.service.archive.ArchiveService;
@ExtendWith(SpringExtension.class)
public class FlingServiceTest {
@Autowired
private FlingService flingService;
@Autowired
private FlingMapper flingMapper;
@MockBean
private PasswordEncoder passwordEncoder;
@MockBean
private FlingRepository flingRepository;
@MockBean
private ArchiveService archiveService;
private FlingEntity flingEntity1;
private FlingEntity flingEntity2;
@TestConfiguration
static class FlingServiceTestConfiguration {
@Bean
public FlingMapper flingMapper() {
return new FlingMapperImpl();
}
@Bean
public ArtifactMapper ArtifactMapper() {
return new ArtifactMapperImpl();
}
@Bean
public FlingService flingService(FlingRepository flingRepository, FlingMapper flingMapper,
ArtifactMapper artifactMapper,
ArchiveService archiveService, PasswordEncoder passwordEncoder) {
return new FlingService(flingRepository, flingMapper, artifactMapper, archiveService,
passwordEncoder);
}
}
@BeforeEach
public void beforeEach() {
this.flingEntity1 = new FlingEntity();
flingEntity1.setId(UUID.randomUUID());
flingEntity1.setName("fling1");
flingEntity1.setAuthCode("testhash");
this.flingEntity2 = new FlingEntity();
flingEntity2.setId(UUID.randomUUID());
flingEntity2.setName("fling2");
flingEntity2.setShareId("shareId2");
when(flingRepository.save(any())).then(new Answer<FlingEntity>() {
@Override
public FlingEntity answer(InvocationOnMock invocation) throws Throwable {
FlingEntity flingEntity = invocation.getArgument(0);
flingEntity.setId(UUID.randomUUID());
return flingEntity;
}
});
}
@Test
public void findAll_noFlings_empty() {
when(flingRepository.findAll()).thenReturn(List.of());
assertThat(flingService.findAll(), is(empty()));
}
@Test
public void findAll_hasFlings_allFlings() {
when(flingRepository.findAll()).thenReturn(List.of(flingEntity1, flingEntity2));
assertThat(flingService.findAll(), hasItems(
flingMapper.map(flingEntity1), flingMapper.map(flingEntity2)));
}
@Test
public void getById_flingDto() {
when(flingRepository.getOne(flingEntity1.getId())).thenReturn(flingEntity1);
assertThat(flingService.getById(flingEntity1.getId()), equalTo(flingMapper.map(flingEntity1)));
}
@Test
public void create_emptyFling_defaultValues() {
FlingDto flingDto = new FlingDto();
FlingDto createdFling = flingService.create(flingDto);
assertThat(createdFling.getShareId(), not(emptyOrNullString()));
assertThat(createdFling.getAuthCode(), emptyOrNullString());
}
@Test
public void create_hasAuthCode_setAuthCode() {
FlingDto flingDto = new FlingDto();
flingDto.setAuthCode("test");
when(passwordEncoder.encode(any(String.class))).thenReturn("testhash");
FlingDto createdFling = flingService.create(flingDto);
assertThat(createdFling.getAuthCode(), is("testhash"));
}
@Test
public void create_hasShareId_setShareId() {
FlingDto flingDto = new FlingDto();
flingDto.setShareId("test");
FlingDto createdFling = flingService.create(flingDto);
assertThat(createdFling.getShareId(), is("test"));
}
@Test
public void replace_newName_expirationClicks_setsNameAndExpirationClicks() {
FlingDto flingDto = FlingDto.builder()
.name("testName")
.authCode(flingEntity1.getAuthCode())
.expirationClicks(2)
.build();
when(flingRepository.getOne(flingEntity1.getId())).thenReturn(flingEntity1);
FlingDto updatedEntity = flingService.replace(flingEntity1.getId(), flingDto);
assertThat(updatedEntity.getId(), is(flingEntity1.getId()));
assertThat(updatedEntity.getName(), is("testName"));
assertThat(updatedEntity.getAuthCode(), is(flingEntity1.getAuthCode()));
assertThat(updatedEntity.getExpirationClicks(), is(2));
}
@Test
public void replace_newAuthCode_setsNewHashedAuthCode() {
FlingDto flingDto = FlingDto.builder()
.name(flingEntity1.getName())
.authCode("new-secret")
.build();
when(passwordEncoder.encode("new-secret")).thenReturn("new-secret-hash");
when(flingRepository.getOne(flingEntity1.getId())).thenReturn(flingEntity1);
FlingDto updatedEntity = flingService.replace(flingEntity1.getId(), flingDto);
verify(passwordEncoder, atLeast(1)).encode("new-secret");
assertThat(updatedEntity.getId(), is(flingEntity1.getId()));
assertThat(updatedEntity.getAuthCode(), is("new-secret-hash"));
assertThat(updatedEntity.getName(), is(flingEntity1.getName()));
}
@Test
public void getByShareId_flingDto() {
when(flingRepository.findByShareId("shareId2")).thenReturn(flingEntity2);
FlingDto foundFling = flingService.getByShareId("shareId2");
assertThat(foundFling.getShareId(), equalTo("shareId2"));
}
@Test
public void getByShareId_noFlingForShareId_throws() {
when(flingRepository.findByShareId(any(String.class))).thenReturn(null);
assertThrows(EntityNotFoundException.class, () -> flingService.getByShareId("doesNotExist"));
}
@Test
public void delete_deletesFromArchiveAndDb() throws IOException {
UUID testId = UUID.randomUUID();
flingService.delete(testId);
verify(archiveService).deleteFling(testId);
verify(flingRepository).deleteById(testId);
}
@Test
public void getArtifacts_noArtifacts_emptySet() throws IOException {
UUID testId = UUID.randomUUID();
FlingEntity flingEntity = new FlingEntity();
flingEntity.setId(testId);
flingEntity.setArtifacts(null);
when(flingRepository.getOne(testId)).thenReturn(flingEntity);
assertThat(flingService.getArtifacts(testId), is(empty()));
}
@Test
public void getArtifacts_flingWithArtifacts_artifactSet() throws Exception {
UUID artifactId = UUID.randomUUID();
ArtifactEntity artifactEntity = new ArtifactEntity();
artifactEntity.setId(artifactId);
UUID flingId = UUID.randomUUID();
FlingEntity flingEntity = new FlingEntity();
flingEntity.setId(flingId);
flingEntity.setArtifacts(Set.of(artifactEntity));
when(flingRepository.getOne(flingId)).thenReturn(flingEntity);
Set<ArtifactDto> artifacts = flingService.getArtifacts(flingId);
assertThat(artifacts, hasSize(1));
assertThat(artifacts.stream().map(ArtifactDto::getId).collect(Collectors.toSet()),
contains(artifactId));
}
@Test
public void validateAuthCode_codesMatch_true() {
when(flingRepository.getOne(flingEntity1.getId())).thenReturn(flingEntity1);
when(passwordEncoder.encode("authCode1")).thenReturn("testhash");
assertThat(flingService.validateAuthCode(flingEntity1.getId(), "authCode1"), is(true));
}
@Test
public void validateAuthCode_codesDoNotMatch_false() {
when(flingRepository.getOne(flingEntity2.getId())).thenReturn(flingEntity2);
assertThat(flingService.validateAuthCode(flingEntity2.getId(), "authCode1"), is(false));
}
}

View file

@ -0,0 +1,305 @@
package net.friedl.fling.service.archive;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import net.friedl.fling.persistence.entities.ArtifactEntity;
import net.friedl.fling.persistence.entities.FlingEntity;
import net.friedl.fling.persistence.repositories.ArtifactRepository;
import net.friedl.fling.service.archive.impl.FileSystemArchive;
@ExtendWith(SpringExtension.class)
public class FileSystemArchiveTest {
@Autowired
private FileSystemArchive fileSystemArchive;
@MockBean
private ArtifactRepository artifactRepository;
private FlingEntity flingEntity1;
private FlingEntity flingEntity2;
private ArtifactEntity artifactEntity1;
private ArtifactEntity artifactEntity2;
@TempDir
static Path tempDir;
@TestConfiguration
static class FlingServiceTestConfiguration {
@Bean
public FileSystemArchive fileSystemArchive(ArtifactRepository artifactRepository)
throws URISyntaxException {
FileSystemArchive fileSystemArchive = new FileSystemArchive(artifactRepository);
fileSystemArchive.setArchivePath(tempDir);
return fileSystemArchive;
}
}
@BeforeEach
public void beforeEach() throws IOException, URISyntaxException {
repopulateArchivePath();
setupTestEntites();
}
@Test
public void getArtifact_flingDiskForFlingIdDoesNotExist_throws() {
flingEntity1.setId(UUID.randomUUID());
artifactEntity1.setFling(flingEntity1);
when(artifactRepository.getOne(artifactEntity1.getId())).thenReturn(artifactEntity1);
assertThrows(IOException.class, () -> fileSystemArchive.getArtifact(artifactEntity1.getId()));
}
@Test
public void getArtifact_returnsArtifact() throws IOException {
when(artifactRepository.getOne(artifactEntity1.getId())).thenReturn(artifactEntity1);
InputStream expectedArtifact =
getClass().getClassLoader().getResourceAsStream("filesystem/artifacts/artifact1");
byte[] expectedArtifactData = expectedArtifact.readAllBytes();
expectedArtifact.close();
InputStream retrievedArtifact = fileSystemArchive.getArtifact(artifactEntity1.getId());
byte[] retrievedArtifactData = retrievedArtifact.readAllBytes();
retrievedArtifact.close();
assertThat(retrievedArtifactData, equalTo(expectedArtifactData));
}
@Test
public void getFling_doesNotExist_throws() {
assertThrows(IOException.class, () -> fileSystemArchive.getFling(UUID.randomUUID()));
}
@Test
public void getFling_returnsFling() throws IOException {
UUID flingUUID = new UUID(0, 0);
InputStream expectedFling = getClass().getClassLoader()
.getResourceAsStream("filesystem/archive_path/" + flingUUID.toString() + ".zip");
byte[] expectedFlingData = expectedFling.readAllBytes();
expectedFling.close();
InputStream retrievedFling = fileSystemArchive.getFling(flingUUID);
byte[] retrievedFlingData = retrievedFling.readAllBytes();
retrievedFling.close();
assertThat(retrievedFlingData, equalTo(expectedFlingData));
}
@Test
public void deleteArtifact_setsArchivedFalse() throws IOException {
when(artifactRepository.getOne(artifactEntity1.getId())).thenReturn(artifactEntity1);
fileSystemArchive.deleteArtifact(artifactEntity1.getId());
assertThat(artifactEntity1.getArchived(), equalTo(false));
InputStream flingStream =
new FileInputStream(
tempDir.resolve(artifactEntity1.getFling().getId().toString() + ".zip").toFile());
ZipInputStream zis = new ZipInputStream(flingStream);
ZipEntry zipEntry;
while ((zipEntry = zis.getNextEntry()) != null) {
assertThat(zipEntry.getName(), not(equalTo(artifactEntity1.getPath().toString())));
zis.closeEntry();
}
zis.close();
}
@Test
public void deleteArtifact_deletesArtifactFromZipDisk() throws IOException {
when(artifactRepository.getOne(artifactEntity1.getId())).thenReturn(artifactEntity1);
fileSystemArchive.deleteArtifact(artifactEntity1.getId());
InputStream flingStream =
new FileInputStream(
tempDir.resolve(artifactEntity1.getFling().getId().toString() + ".zip").toFile());
ZipInputStream zis = new ZipInputStream(flingStream);
ZipEntry zipEntry;
while ((zipEntry = zis.getNextEntry()) != null) {
assertThat(zipEntry.getName(), not(equalTo(artifactEntity1.getPath().toString())));
zis.closeEntry();
}
zis.close();
}
@Test
public void storeArtifact_setsArchivedTrue() throws IOException, URISyntaxException {
InputStream artifact2Stream = new FileInputStream(
new File(
getClass().getClassLoader().getResource("filesystem/artifacts/artifact2").toURI()));
when(artifactRepository.getOne(artifactEntity2.getId())).thenReturn(artifactEntity2);
fileSystemArchive.storeArtifact(artifactEntity2.getId(), artifact2Stream);
artifact2Stream.close();
assertThat(artifactEntity2.getArchived(), equalTo(true));
}
@Test
public void storeArtifact_storesArtifactToFlingDisk() throws URISyntaxException, IOException {
InputStream artifact2Stream = new FileInputStream(
new File(
getClass().getClassLoader().getResource("filesystem/artifacts/artifact2").toURI()));
when(artifactRepository.getOne(artifactEntity2.getId())).thenReturn(artifactEntity2);
fileSystemArchive.storeArtifact(artifactEntity2.getId(), artifact2Stream);
artifact2Stream.close();
InputStream flingStream =
new FileInputStream(
tempDir.resolve(artifactEntity2.getFling().getId().toString() + ".zip").toFile());
ZipInputStream zis = new ZipInputStream(flingStream);
ZipEntry zipEntry;
List<String> diskEntries = new LinkedList<>();
while ((zipEntry = zis.getNextEntry()) != null) {
diskEntries.add(zipEntry.getName());
zis.closeEntry();
}
zis.close();
assertThat(diskEntries, hasItem(Path.of("/").relativize(artifactEntity2.getPath()).toString()));
}
@Test
public void deleteFling_setsArchivedFalseForAllContainedArtifacts() throws IOException {
when(artifactRepository.findAllByFlingId(artifactEntity1.getFling().getId()))
.thenReturn(List.of(artifactEntity1));
fileSystemArchive.deleteFling(artifactEntity1.getFling().getId());
assertThat(artifactEntity1.getArchived(), equalTo(false));
}
@Test
public void deleteFling_deletesZipDisk() throws IOException {
assertThat(Files.exists(tempDir.resolve(artifactEntity1.getFling().getId() + ".zip")),
equalTo(true));
fileSystemArchive.deleteFling(artifactEntity1.getFling().getId());
assertThat(Files.exists(tempDir.resolve(artifactEntity1.getFling().getId() + ".zip")),
equalTo(false));
}
@Test
public void deleteFling_zipDiskNotFound_noThrow() throws IOException {
assertThat(Files.exists(tempDir.resolve(artifactEntity2.getFling().getId() + ".zip")),
equalTo(false));
assertDoesNotThrow(() -> fileSystemArchive.deleteFling(artifactEntity2.getFling().getId()));
}
private void repopulateArchivePath() throws IOException, URISyntaxException {
Files.walkFileTree(tempDir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException e)
throws IOException {
if (e == null) {
Files.delete(dir);
return FileVisitResult.CONTINUE;
} else {
// directory iteration failed
throw e;
}
}
});
Path source =
Path.of(getClass().getClassLoader().getResource("filesystem/archive_path").toURI());
Path target = tempDir;
Files.walkFileTree(source, Set.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
Path targetdir = target.resolve(source.relativize(dir));
try {
Files.copy(dir, targetdir);
} catch (FileAlreadyExistsException e) {
if (!Files.isDirectory(targetdir))
throw e;
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
Files.copy(file, target.resolve(source.relativize(file)));
return FileVisitResult.CONTINUE;
}
});
}
private void setupTestEntites() {
// Fling1/Artifact1
this.artifactEntity1 = new ArtifactEntity();
artifactEntity1.setId(UUID.randomUUID());
artifactEntity1.setPath(Path.of("artifact1"));
artifactEntity1.setArchived(true);
this.flingEntity1 = new FlingEntity();
flingEntity1.setId(new UUID(0, 0));
flingEntity1.setName("fling1");
artifactEntity1.setFling(flingEntity1);
// Fling2/Artifact2
this.artifactEntity2 = new ArtifactEntity();
artifactEntity2.setId(UUID.randomUUID());
artifactEntity2.setPath(Path.of("/", "/sub", "artifact2"));
artifactEntity2.setArchived(false);
this.flingEntity2 = new FlingEntity();
flingEntity2.setId(new UUID(1, 0));
flingEntity2.setName("fling2");
artifactEntity2.setFling(flingEntity2);
}
}

View file

@ -0,0 +1,3 @@
fling.security.jwt-expiration=18000
fling.security.admin-name=admin
fling.security.admin-password=123

View file

@ -0,0 +1 @@
artifact1 ok

View file

@ -0,0 +1 @@
artifact2 ok

View file

@ -10,13 +10,4 @@
<password>${env.NEXUS_PASSWORD}</password> <password>${env.NEXUS_PASSWORD}</password>
</server> </server>
</servers> </servers>
<mirrors>
<mirror>
<!--This sends everything else to /public -->
<id>nexus</id>
<mirrorOf>central</mirrorOf>
<url>https://nexus.friedl.net/repository/maven-central/</url>
</mirror>
</mirrors>
</settings> </settings>

View file

@ -1,2 +1,2 @@
REACT_APP_API=https://fling.friedl.net/api REACT_APP_API=https://fling.friedl.net
REACT_APP_LOGLEVEL=warn REACT_APP_LOGLEVEL=warn

View file

@ -1,2 +1,2 @@
REACT_APP_API=http://localhost:8080/api REACT_APP_API=http://localhost:8080/
REACT_APP_LOGLEVEL=trace REACT_APP_LOGLEVEL=trace

1
web/fling/.npmrc Normal file
View file

@ -0,0 +1 @@
@fling:registry=https://nexus.friedl.net/repository/npm-private/

File diff suppressed because it is too large Load diff

View file

@ -3,15 +3,17 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@fling/flingclient": "0.1.0-snapshot",
"@testing-library/jest-dom": "^4.2.4", "@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0", "@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1", "@testing-library/user-event": "^7.2.1",
"axios": "^0.19.2", "axios": "^0.19.2",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"immer": "^7.0.7",
"jwt-decode": "^2.2.0", "jwt-decode": "^2.2.0",
"loglevel": "^1.6.8", "loglevel": "^1.6.8",
"node-sass": "^4.14.0", "node-sass": "^4.14.1",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"react": "^16.13.1", "react": "^16.13.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
@ -19,6 +21,7 @@
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
"react-scripts": "3.4.1", "react-scripts": "3.4.1",
"redux": "^4.0.5", "redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"spectre.css": "^0.5.8" "spectre.css": "^0.5.8"
}, },
"scripts": { "scripts": {

View file

@ -1,9 +1,9 @@
import log from 'loglevel'; import log from 'loglevel';
import React from 'react'; import React from 'react';
import {Switch, Route, Redirect} from "react-router-dom"; import { Switch, Route, Redirect } from "react-router-dom";
import request, {isOwner, isUser} from './util/request'; import jwt from './util/jwt.js';
import Login from './components/admin/Login'; import Login from './components/admin/Login';
import FlingAdmin from './components/admin/FlingAdmin'; import FlingAdmin from './components/admin/FlingAdmin';
@ -12,54 +12,86 @@ import Unlock from './components/user/Unlock';
import FlingUser from './components/user/FlingUser'; import FlingUser from './components/user/FlingUser';
import LandingPage from './components/LandingPage'; import LandingPage from './components/LandingPage';
/**
* Front routes, defaults to a 404 Page.
* Routes:
* - / : Landing page
* - /admin/login : A login page. Redirects with admin token upon successful
login
* - /admin : The fling administration page. Redirects to a login page if not
authenticated
* - /admin/[fling id]/* : Go directly to a fling (sub-)page. Redirects to a
login page if not authenticated
* - /unlock : A unlock page. Redirects with user token upon successful login.
* - /f/[shareId] : Opens a fling page for a user
*/
export default () => { export default () => {
return ( return (
<Switch> <Switch>
<Route exact path="/" component={LandingPage} /> <Route exact path="/" component={LandingPage} />
<Route exact path="/admin/login" component={Login} /> <Route exact path="/admin/login" component={Login} />
<OwnerRoute exact path="/admin"><FlingAdmin /></OwnerRoute> <OwnerRoute exact path="/admin"><FlingAdmin /></OwnerRoute>
<OwnerRoute path="/admin/:fling"><FlingAdmin /></OwnerRoute> <OwnerRoute path="/admin/:flingId"><FlingAdmin /></OwnerRoute>
<Route exact path="/unlock" component={Unlock} /> <Route exact path="/unlock" component={Unlock} />
<UserRoute path="/f/:shareId"><FlingUser /></UserRoute> <UserRoute path="/f/:shareId"><FlingUser /></UserRoute>
<Route match="*">Not implemented</Route> <Route match="*">Not implemented</Route>
</Switch> </Switch>
); );
} }
// A wrapper for <Route> that redirects to the login /*
// screen if you're not yet authenticated. * A wrapper for <Route> that redirects to the login screen if no admin
* authentication token was found.
*
* Note that the token check is purely client-side. It provides no actual
* protection! It is hence possible to reach the admin site with some small
* amount of trickery. Without a valid token no meaningful actions are possible
* on the admin page though.
*/
function OwnerRoute({ children, ...rest }) { function OwnerRoute({ children, ...rest }) {
return ( log.info(`Routing request for ${rest['path']}`);
<Route return (
{...rest} <Route
render={({ location }) => { {...rest}
log.info(request.defaults); render={({ location }) => {
if(isOwner()) { return children; } if (jwt.hasSubject("admin")) { return children; }
else { return <Redirect to={{pathname: "/admin/login", state: {from: location}}} />; } else {
}} return <Redirect to={{
/> pathname: "/admin/login",
); state: { from: location }
}} />;
}
}}
/>
);
} }
// A wrapper for <Route> that redirects to the unlock /* A wrapper for <Route> that redirects to the unlock screen if no authorized
// screen if the fling is protected * token * was found.
*
* Note that the token check is purely client-side. It provides no actual
* protection! It is hence possible to reach the target site with some small
* amount of trickery. Without a valid token, no meaningful actions are possible
* on the target page though - this must be checked server side.
*/
function UserRoute({ children, ...rest }) { function UserRoute({ children, ...rest }) {
return ( log.debug(`Routing request for ${rest['path']}`);
<Route return (
{...rest} <Route
render={({ match, location }) => { {...rest}
log.info(request.defaults); render={({ match, location }) => {
log.info(match); let state = { from: location, shareId: match.params.shareId };
log.info(location);
let x = {from: location, shareId: match.params.shareId};
if(isOwner()) { return children; } let authorized =
else if(isUser(match.params.shareId)) { return children; } jwt.hasSubject("admin")
else { return <Redirect to={ {pathname: "/unlock", state: x} } />; } || ( jwt.hasSubject("user") && jwt.hasClaim("shareId", state['shareId']) );
}}
/> if (authorized) { return children; }
); else { return <Redirect to={{ pathname: "/unlock", state: state }} />; }
}}
/>
);
} }

View file

@ -13,7 +13,6 @@ export default function LandingPage() {
function openFling(ev) { function openFling(ev) {
ev.preventDefault(); ev.preventDefault();
window.location = `/f/${shareId}`; window.location = `/f/${shareId}`;
} }

View file

@ -1,23 +0,0 @@
import React from 'react';
export default (props) => {
function renderError() {
return (
<div className="toast toast-error mb-2">
<button className="btn btn-clear float-right" onClick={props.clearErrors}></button>
<h5>Ooops!</h5>
<li>
{ props.errors.map( (err, idx) => <ul key={idx}>{err}</ul> ) }
</li>
</div>
);
}
return (
<>
{ props.errors.length > 0 && !props.below ? renderError() : "" }
{ props.children }
{ props.errors.length > 0 && props.below ? renderError() : "" }
</>
);
}

View file

@ -1,24 +1,41 @@
import React from 'react'; import React, { useEffect } from 'react';
import { useDispatch } from "react-redux";
import { useParams } from 'react-router-dom';
import { retrieveFlings, setActiveFling } from "../../redux/actions";
import Navbar from './Navbar'; import Navbar from './Navbar';
import FlingList from './FlingList'; import FlingList from './FlingList';
import FlingContent from './FlingContent'; import FlingContent from './FlingContent';
import {useParams} from 'react-router-dom';
export default function FlingAdmin() { export default function FlingAdmin() {
let { fling } = useParams(); const { flingId } = useParams();
const dispatch = useDispatch();
return( useEffect(() => {
<div> dispatch(retrieveFlings());
<Navbar /> }, [dispatch]);
<div className="container"> useEffect(() => {
<div className="columns mt-2"> if (flingId) {
<div className="column col-sm-12 col-lg-3 col-2"> <FlingList activeFling={fling} /> </div> dispatch(setActiveFling(flingId));
<div className="column col-sm-12"><FlingContent activeFling={fling} /></div> }
</div> }, [flingId, dispatch]);
return (
<div>
<Navbar />
<div className="container">
<div className="columns mt-2">
<div className="column col-sm-12 col-lg-3 col-2">
<FlingList />
</div>
<div className="column col-sm-12">
<FlingContent />
</div> </div>
</div> </div>
); </div>
</div>
);
} }

View file

@ -1,114 +1,109 @@
import log from 'loglevel'; import log from 'loglevel';
import React, {useState, useEffect, useRef} from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import {artifactClient} from '../../util/flingclient'; import { ArtifactClient, FlingClient, AuthClient } from '../../util/fc';
import { prettifyTimestamp } from '../../util/fn';
function FlingArtifactControl(props) { function FlingArtifactControl(props) {
let iframeContainer = useRef(null); let iframeContainer = useRef(null);
const artifactClient = new ArtifactClient();
const authClient = new AuthClient();
function handleDelete(ev) { function handleDelete(ev) {
artifactClient.deleteArtifact(props.artifact.id) artifactClient.deleteArtifact(props.artifact.id)
.then(() => props.reloadArtifactsFn()); .then(() => props.reloadArtifactsFn());
} }
function handleDownload(ev) {
authClient.deriveToken({ singleUse: true })
.then(token => {
// We need this iframe hack because with a regular href, while
// the browser downloads the file fine, it also reloads the page, hence
// loosing all logs and state
let frame = document.createElement("iframe");
let url = `${process.env.REACT_APP_API.replace(/\/+$/, '')}/api/artifacts/${props.artifact.id}/data?derivedToken=${token}`;
log.trace(`Generated download url: ${url}`);
frame.src = url;
iframeContainer.current.appendChild(frame);
});
}
function handleDownload(ev) { return (
artifactClient.downloadArtifact(props.artifact.id) <div className={`btn-group ${props.hidden ? "d-invisible" : "d-visible"}`}>
.then(url => { <button className="btn btn-sm" onClick={handleDelete}><i className="icon icon-delete" /></button>
// We need this iframe hack because with a regular href, while <button className="btn btn-sm"><i className="icon icon-edit" /></button>
// the browser downloads the file fine, it also reloads the page, hence <button className="btn btn-sm" onClick={handleDownload}><i className="icon icon-download" /></button>
// loosing all logs and state <div className="d-hide" ref={iframeContainer} />
let frame = document.createElement("iframe"); </div>
frame.src = url; );
iframeContainer.current.appendChild(frame);
});
}
return(
<div className={`btn-group ${props.hidden ? "d-invisible": "d-visible"}`}>
<button className="btn btn-sm" onClick={handleDelete}><i className="icon icon-delete"/></button>
<button className="btn btn-sm"><i className="icon icon-edit"/></button>
<button className="btn btn-sm" onClick={handleDownload}><i className="icon icon-download"/></button>
<div className="d-hide" ref={iframeContainer}/>
</div>
);
} }
function FlingArtifactRow(props) { function FlingArtifactRow(props) {
let [hovered, setHovered] = useState(false); let [hovered, setHovered] = useState(false);
function readableBytes(bytes) {
if(bytes <= 0) return "0 KB";
var i = Math.floor(Math.log(bytes) / Math.log(1024)), return (
sizes = ['Byte', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; <tr key={props.artifact.id} className="artifact-row" onMouseOver={() => setHovered(true)} onMouseOut={() => setHovered(false)}>
<td>{props.artifact.path}</td>
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i]; <td>{prettifyTimestamp(props.artifact.creationTime, true)}</td>
} <td></td>
<td><FlingArtifactControl artifact={props.artifact} reloadArtifactsFn={props.reloadArtifactsFn} hidden={!hovered} /></td>
function localizedUploadDate() { </tr>
let d = new Date(props.artifact.uploadTime); );
return d.toLocaleDateString();
}
return(
<tr key={props.artifact.id} className="artifact-row" onMouseOver={() => setHovered(true)} onMouseOut={() => setHovered(false)}>
<td>{props.artifact.name}</td>
<td>{localizedUploadDate()}</td>
<td>{readableBytes(props.artifact.size)}</td>
<td><FlingArtifactControl artifact={props.artifact} reloadArtifactsFn={props.reloadArtifactsFn} hidden={!hovered} /></td>
</tr>
);
} }
function FlingInfo(props) { function FlingInfo(props) {
return( return (
<div className="m-2"> <div className="m-2">
{ /* Add some infos about the fling */ } { /* Add some infos about the fling */}
</div> </div>
); );
} }
export default function FlingArtifacts(props) { export default function FlingArtifacts() {
const [artifacts, setArtifacts] = useState([]); const flingClient = new FlingClient();
useEffect(getArtifacts, [props.activeFling]); const activeFling = useSelector(state => state.flings.activeFling);
const [artifacts, setArtifacts] = useState([]);
return ( useEffect(getArtifacts, [activeFling]);
<div>
<FlingInfo />
<table className="table"> return (
<thead> <div>
<tr> <FlingInfo />
<th>Name</th>
<th>Uploaded</th>
<th>Size</th>
<th/>
</tr>
</thead>
<tbody>
{artifacts}
</tbody>
</table>
</div>
);
function getArtifacts() { <table className="table">
if (!props.activeFling) { <thead>
log.debug("No fling active. Not getting artifacts."); <tr>
return; <th>Name</th>
<th>Uploaded</th>
<th>Size</th>
<th />
</tr>
</thead>
<tbody>
{artifacts}
</tbody>
</table>
</div>
);
function getArtifacts() {
if (!activeFling) {
log.debug("No fling active. Not getting artifacts.");
return;
}
log.debug(`Fling ${activeFling} active. Getting artifacts.`);
let artifacts = [];
flingClient.getArtifacts(activeFling.id)
.then(result => {
log.debug(`Got ${result.length} artifacts`);
for (let artifact of result) {
artifacts.push(<FlingArtifactRow key={artifact.id} artifact={artifact} reloadArtifactsFn={getArtifacts} />);
} }
log.debug(`Fling ${props.activeFling} active. Getting artifacts.`); setArtifacts(artifacts);
let artifacts = []; });
}
artifactClient.getArtifacts(props.activeFling)
.then(result => {
log.debug(`Got ${result.length} artifacts`);
for(let artifact of result) {
artifacts.push(<FlingArtifactRow key={artifact.id} artifact={artifact} reloadArtifactsFn={getArtifacts} />);
}
setArtifacts(artifacts);
});
}
} }

View file

@ -1,42 +1,65 @@
import log from 'loglevel'; import log from 'loglevel';
import React from 'react'; import React from 'react';
import {Switch, Route, useLocation, Link} from "react-router-dom"; import { Switch, Route, useLocation, Link } from "react-router-dom";
import { useSelector } from "react-redux";
import FlingArtifacts from './FlingArtifacts'; import FlingArtifacts from './FlingArtifacts';
import Upload from './Upload'; import Upload from './Upload';
import Settings from './Settings'; import Settings from './Settings';
export default function FlingContent(props) { export default function FlingContent() {
let location = useLocation(); const location = useLocation();
const activeFling = useSelector(state => state.flings.activeFling);
function Empty() {
return (
<div className="empty">
<div className="empty-icon">
<i className="icon icon-search icon-2x"></i>
</div>
<p className="empty-title h5">No Fling selected</p>
<p className="empty-subtitle">Select a fling from the list</p>
</div>
);
}
function Content() {
function path(tail) { function path(tail) {
return `/admin/${props.activeFling}/${tail}`; return `/admin/${activeFling.id}/${tail}`;
} }
return( return (
<div className="fling-content p-2"> <div className="fling-content p-2">
{log.info("FlingContent location ", location)} {log.info("FlingContent location ", location)}
{log.info("FlingContent active fling ", props.activeFling)} {log.info("FlingContent active fling ", activeFling)}
<ul className="tab tab-block mt-0"> <ul className="tab tab-block mt-0">
<li className={`tab-item ${location.pathname !== path("upload") && location.pathname !== path("settings") ? "active": ""}`}> <li className={`tab-item ${location.pathname !== path("upload") && location.pathname !== path("settings") ? "active" : ""}`}>
<Link to={path("files")}>Files</Link> <Link to={path("files")}>Files</Link>
</li> </li>
<li className={`tab-item ${location.pathname === path("upload") ? "active": ""}`}> <li className={`tab-item ${location.pathname === path("upload") ? "active" : ""}`}>
<Link to={path("upload")}>Upload</Link> <Link to={path("upload")}>Upload</Link>
</li> </li>
<li className={`tab-item ${location.pathname === path("settings") ? "active": ""}`}> <li className={`tab-item ${location.pathname === path("settings") ? "active" : ""}`}>
<Link to={path("settings")}>Settings</Link> <Link to={path("settings")}>Settings</Link>
</li> </li>
</ul> </ul>
<div className="mt-2"> <div className="mt-2">
<Switch> <Switch>
<Route exact path="/admin/:fling"><FlingArtifacts activeFling={props.activeFling} /></Route> <Route exact path="/admin/:fling"><FlingArtifacts /></Route>
<Route path="/admin/:fling/files"><FlingArtifacts activeFling={props.activeFling} /></Route> <Route path="/admin/:fling/files"><FlingArtifacts /></Route>
<Route path="/admin/:fling/upload"><Upload activeFling={props.activeFling} /></Route> <Route path="/admin/:fling/upload"><Upload /></Route>
<Route path="/admin/:fling/settings"><Settings activeFling={props.activeFling} /></Route> <Route path="/admin/:fling/settings"><Settings /></Route>
</Switch> </Switch>
</div>
</div> </div>
</div>
); );
}
return (
<>
{ activeFling ? Content() : Empty() }
</>
);
} }

View file

@ -1,37 +1,19 @@
import log from 'loglevel'; import React from 'react';
import React, {useState, useEffect} from 'react'; import { useSelector } from "react-redux";
import {flingClient} from '../../util/flingclient';
import FlingTile from './FlingTile'; import FlingTile from './FlingTile';
export default function FlingList(props) { export default function FlingList() {
const [flings, setFlings] = useState([]); const flings = useSelector((store) => store.flings.flings);
useEffect(() => { (async () => {
let flings = await flingClient.getFlings();
let newFlings = [];
for (let fling of flings) { return (
let flingTile = <FlingTile fling={fling} <div className="panel">
key={fling.id} <div className="panel-header p-2">
refreshFlingListFn={refreshFlingList} />; <h5>My Flings</h5>
newFlings.push(flingTile); </div>
} <div className="panel-body p-0">
setFlings(newFlings); {flings.map(fling => <FlingTile fling={fling} key={fling.id} />)}
})(); } , []); </div>
</div>
return( );
<div className="panel">
{log.info(`Got active fling: ${props.activeFling}`)}
<div className="panel-header p-2">
<h5>My Flings</h5>
</div>
<div className="panel-body p-0">
{flings}
</div>
</div>
);
async function refreshFlingList() {
}
} }

View file

@ -1,84 +1,89 @@
import log from 'loglevel'; import log from 'loglevel';
import React, {useRef} from 'react'; import React, { useRef } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import {NavLink} from "react-router-dom"; import { NavLink } from "react-router-dom";
import {flingClient} from '../../util/flingclient'; import { useSelector, useDispatch } from "react-redux";
import { deleteFling } from "../../redux/actions";
function TileAction(props) { function TileAction(props) {
let shareUrlRef = useRef(null); let shareUrlRef = useRef(null);
const dispatch = useDispatch();
return( function copyShareUrl() {
<div className="tile-action dropdown"> shareUrlRef.current.focus();
<button className="btn btn-link btn dropdown-toggle" tabIndex="0"> shareUrlRef.current.select();
<i className="icon icon-more-vert" />
try {
let successful = document.execCommand('copy');
let msg = successful ? 'successful' : 'unsuccessful';
console.log('Copying to clipoard ' + msg);
} catch (err) {
log.error("Couldn't copy to clipboard: ", err);
}
}
return (
<div className="tile-action dropdown">
<button className="btn btn-link btn dropdown-toggle" tabIndex="0">
<i className="icon icon-more-vert" />
</button>
<ul className="menu text-left">
<li className="menu-item input-group">
<div className="input-group">
<input type="text" ref={shareUrlRef}
className="form-input input-sm input-share-id" readOnly
value={props.fling.shareId} />
<span className="input-group-addon addon-sm input-group-addon-sm"
onClick={copyShareUrl}>
<i className="icon icon-copy" /></span>
</div>
</li>
<li className="menu-item">
<div className="form-group">
<label className="form-switch">
<input type="checkbox" disabled checked={props.fling.shared} />
<i className="form-icon" />
{props.fling.shared ? "Shared" : "Private"}
</label>
</div>
</li>
<li className="menu-item">
<button className="btn btn-link text-warning pl-0"
onClick={ev => dispatch(deleteFling(props.fling.id))}>
<i className="icon icon-delete mr-1" /> Remove
</button> </button>
<ul className="menu text-left"> </li>
<li className="menu-item input-group"> </ul>
<div className="input-group"> </div>
<input type="text" ref={shareUrlRef} className="form-input input-sm input-share-id" readOnly value={props.fling.sharing.shareUrl} /> );
<span className="input-group-addon addon-sm input-group-addon-sm" onClick={copyShareUrl} ><i className="icon icon-copy" /></span>
</div>
</li>
<li className="menu-item">
<div className="form-group">
<label className="form-switch">
<input type="checkbox" checked={props.fling.sharing.shared} onChange={toggleShared} />
<i className="form-icon" />
{props.fling.sharing.shared ? "Shared":"Private"}
</label>
</div>
</li>
<li className="menu-item">
<button className="btn btn-link text-warning pl-0" onClick={deleteFling}>
<i className="icon icon-delete mr-1" /> Remove
</button>
</li>
</ul>
</div>
);
function copyShareUrl() {
shareUrlRef.current.focus();
shareUrlRef.current.select();
try {
let successful = document.execCommand('copy');
let msg = successful ? 'successful' : 'unsuccessful';
console.log('Copying to clipoard ' + msg);
} catch (err) {
log.error("Couldn't copy to clipboard: ", err);
}
}
async function deleteFling() {
await flingClient.deleteFling(props.fling.id);
await props.refreshFlingListFn();
}
async function toggleShared() {
await flingClient.putFling(props.fling.id, {"sharing": {"shared": !props.fling.sharing.shared}});
await props.refreshFlingListFn();
}
} }
export default function FlingTile(props) { export default function FlingTile(props) {
let tileClasses = classNames( const activeFling = useSelector((state) => state.flings.activeFling);
"tile", "tile-centered", "p-2", "c-hand",
{"active": props.activeFling === props.fling.id}
);
return ( let tileClasses = classNames(
<div> "tile", "tile-centered", "p-2", "c-hand",
<div className={tileClasses}> { "active": activeFling ? activeFling.id === props.fling.id : false }
<div className="tile-content"> );
<NavLink to={`/admin/${props.fling.id}`}>
<div className="tile-title">{props.fling.name}</div> return (
<small className="tile-subtitle text-gray">14MB · Public · 1 Jan, 2017</small> <div>
</NavLink> <div className={tileClasses}>
</div> <div className="tile-content">
<TileAction fling={props.fling} refreshFlingListFn={props.refreshFlingListFn} /> <NavLink to={`/admin/${props.fling.id}`}>
</div> <div className="tile-title">{props.fling.name}</div>
<small className="tile-subtitle text-gray">
{`${props.fling.shared ? "Shared" : "Private"}` +
` · ${(new Date(props.fling.creationTime)).toLocaleDateString()}` +
` · ${props.fling.authCode ? "Protected" : "Unprotected"}`}
</small>
</NavLink>
</div> </div>
); <TileAction fling={props.fling} />
</div>
</div>
);
} }

View file

@ -2,87 +2,62 @@ import log from 'loglevel';
import React, {useState, useEffect} from 'react'; import React, {useState, useEffect} from 'react';
import {useHistory, useLocation} from 'react-router-dom'; import {useHistory, useLocation} from 'react-router-dom';
import request, {setAuth} from '../../util/request'; import {fc, AuthClient} from '../../util/fc';
import Error from './Error';
export default function Login() { export default function Login() {
const [errors, setErrors] = useState([]); const [username, setUsername] = useState("");
const [username, setUsername] = useState(""); const [password, setPassword] = useState("");
const [password, setPassword] = useState(""); const history = useHistory();
const history = useHistory(); const location = useLocation();
const location = useLocation(); const { from } = location.state || { from: { pathname: "/admin" } };
const { from } = location.state || { from: { pathname: "/admin" } };
useEffect(() => setAuth(null), []); useEffect(() => {
sessionStorage.removeItem("token")
});
return ( function handleSubmit(ev) {
<div className="container-center"> ev.preventDefault();
<div>
<Error errors={errors} clearErrors={clearErrors} >
<form className="login-form" onSubmit={handleSubmit}>
<div className="form-group">
<label className="form-label" htmlFor="username">Username</label>
<input className="form-input" id="username" name="username" type="text" placeholder="Username"
value={username} onChange={handleChange} />
</div>
<div className="form-group">
<label className="form-label" htmlFor="password">Password</label>
<input className="form-input" id="password" name="password" type="password" placeholder={"*".repeat(18)}
value={password} onChange={handleChange} />
</div>
<div className="login-action-row">
<div className="form-group">
<label className="form-switch input-sm">
<input type="checkbox" />
<i className="form-icon" /> Remember me
</label>
</div>
<button className="btn btn-primary" type="submit">Sign In</button>
</div>
</form>
</Error>
<p className="login-footer">Ready. Set. Fling.</p> let authClient = new AuthClient();
let opt = {adminAuth: new fc.AdminAuth(username, password)};
authClient.authenticateOwner(opt)
.then(response => {
log.info("Login successful");
sessionStorage.setItem('token', response);
log.debug("Returning back to", from);
history.replace(from);
}).catch(log.error);
};
return (
<div className="container-center">
<div>
<form className="login-form" onSubmit={handleSubmit}>
<div className="form-group">
<label className="form-label" htmlFor="username">Username</label>
<input className="form-input" id="username" name="username" type="text" placeholder="Username"
value={username} onChange={ev => setUsername(ev.currentTarget.value)} />
</div> </div>
<div className="form-group">
<label className="form-label" htmlFor="password">Password</label>
<input className="form-input" id="password" name="password" type="password" placeholder={"*".repeat(18)}
value={password} onChange={ev => setPassword(ev.currentTarget.value)} />
</div>
<div className="login-action-row">
<div className="form-group">
<label className="form-switch input-sm">
<input type="checkbox" />
<i className="form-icon" /> Remember me
</label>
</div>
<button className="btn btn-primary" type="submit">Sign In</button>
</div>
</form>
</div> <p className="login-footer">Ready. Set. Fling.</p>
); </div>
function handleSubmit(ev) { </div>
ev.preventDefault(); );
request.post("/auth/owner", {'username': username, 'password': password})
.then(response => {
log.info("Logged in successfully");
setAuth(response.data);
history.replace(from);
})
.catch(error => {
log.error(error);
let response = error.response;
response.data && response.data.message && setErrors( prev => [response.data.message, ...prev] );
});
};
function handleChange(ev) {
let name = ev.target.name;
let val = ev.target.value;
switch(name) {
case "username":
setUsername(val);
break;
case "password":
setPassword(val);
break;
default:
log.error(`Cannot handle change ${name}`);
break;
}
};
function clearErrors() {
setErrors([]);
}
} }

View file

@ -1,242 +1,259 @@
import log from 'loglevel'; import log from 'loglevel';
import React, {useState} from 'react'; import React, { useState } from 'react';
import {flingClient} from '../../util/flingclient'; import { FlingClient, fc } from '../../util/fc';
export default function New(props) { export default function New(props) {
let defaultState = () => ({name: "", authCode: "", let defaultState = () => ({
sharing: {directDownload: true, allowUpload: false, shared: true, shareUrl: ""}, name: "", authCode: "",
expiration: {}}); sharing: { directDownload: true, allowUpload: false, shared: true, shareUrl: "" },
expiration: {}
});
let [fling, setFling] = useState(defaultState()); let [fling, setFling] = useState(defaultState());
let [shareUrlUnique, setShareUrlUnique] = useState(true); let [shareUrlUnique, setShareUrlUnique] = useState(true);
function toggleSharing(ev) { function toggleSharing(ev) {
let f = {...fling}; let f = { ...fling };
let s = {...fling.sharing}; let s = { ...fling.sharing };
if(ev.currentTarget.id === "direct-download") { if (ev.currentTarget.id === "direct-download") {
if(ev.currentTarget.checked) { if (ev.currentTarget.checked) {
s.directDownload = true; s.directDownload = true;
s.shared = true; s.shared = true;
s.allowUpload = false; s.allowUpload = false;
} else { } else {
s.directDownload = false; s.directDownload = false;
} }
} else if(ev.currentTarget.id === "allow-upload") { } else if (ev.currentTarget.id === "allow-upload") {
if(ev.currentTarget.checked) { if (ev.currentTarget.checked) {
s.allowUpload = true; s.allowUpload = true;
s.shared = true; s.shared = true;
s.directDownload = false; s.directDownload = false;
} else { } else {
s.allowUpload = false; s.allowUpload = false;
} }
} else if(ev.currentTarget.id === "shared") { } else if (ev.currentTarget.id === "shared") {
if(!ev.currentTarget.checked) { if (!ev.currentTarget.checked) {
s.allowUpload = s.directDownload = s.shared = false; s.allowUpload = s.directDownload = s.shared = false;
} else { } else {
s.shared = true; s.shared = true;
} }
}
f.sharing = s;
setFling(f);
} }
function handleClose(ev) { f.sharing = s;
if(ev) ev.preventDefault(); // this is needed, otherwise a submit event is fired
props.closeModalFn(); setFling(f);
}
function handleClose(ev) {
if (ev) ev.preventDefault(); // this is needed, otherwise a submit event is fired
props.closeModalFn();
}
function setShareUrl(ev) {
let f = { ...fling };
let s = { ...fling.sharing }; //TODO: expiration is not cloned
let value = ev.currentTarget.value;
if (!value) {
setShareUrlUnique(false);
s.shareUrl = value;
f.sharing = s;
setFling(f);
return;
} }
function setShareUrl(ev) { const flingClient = new FlingClient();
let f = {...fling}; flingClient.getFlingByShareId(ev.currentTarget.value)
let s = {...fling.sharing}; //TODO: expiration is not cloned .then(result => setShareUrlUnique(false))
let value = ev.currentTarget.value; .catch(error => error.status === 404 && setShareUrlUnique(true) )
.finally(() => { s.shareUrl = value; f.sharing = s; setFling(f); });
}
if(!value) { function setName(ev) {
setShareUrlUnique(false); let f = { ...fling };
s.shareUrl = value; let value = ev.currentTarget.value;
f.sharing = s;
setFling(f);
return;
}
flingClient.getFlingByShareId(ev.currentTarget.value) f.name = value;
.then(result => { setFling(f);
if(!result) { }
setShareUrlUnique(true);
} else {
setShareUrlUnique(false);
}
s.shareUrl = value; function setExpirationType(ev) {
f.sharing = s; let f = { ...fling };
setFling(f); let e = { ...fling.expiration }; //TODO: sharing is not cloned
}); let value = ev.currentTarget.value;
if (value === "never") {
e = {};
} else {
e.type = value;
e.value = "";
} }
function setName(ev) { f.expiration = e;
let f = {...fling}; setFling(f);
let value = ev.currentTarget.value; }
f.name = value; function setExpirationValue(ev) {
setFling(f); let f = { ...fling };
let e = { ...fling.expiration }; //TODO: sharing is not cloned
let value = e.type === "time" ? ev.currentTarget.valueAsNumber : ev.currentTarget.value;
e.value = value;
f.expiration = e;
setFling(f);
}
function formatExpirationTime() {
if (!fling.expiration || !fling.expiration.value || fling.expiration.type !== "time")
return "";
let date = new Date(fling.expiration.value);
let fmt = date.toISOString().split("T")[0];
return fmt;
}
function setAuthCode(ev) {
let f = { ...fling };
let value = ev.currentTarget.value;
f.authCode = value;
setFling(f);
}
function handleSubmit(ev) {
ev.preventDefault();
log.info("Creating new filing");
const flingClient = new FlingClient();
let flingEntity = new fc.Fling(fling.name);
flingEntity.directDownload = fling.sharing.directDownload;
flingEntity.allowUpload = fling.sharing.allowUpload;
flingEntity.shared = fling.sharing.shared;
flingEntity.shareId = fling.sharing.shareUrl;
flingEntity.authCode = fling.authCode;
if (fling.expiration.type) {
switch (fling.expiration.type) {
case "time":
flingEntity.expirationTime = fling.expiration.value;
break;
case "clicks":
flingEntity.expirationClicks = fling.expiration.value;
break;
default:
log.warn("Unknown expiration type");
break;
}
} }
function setExpirationType(ev) { flingClient.postFling({ fling: flingEntity })
let f = {...fling}; .then(() => handleClose())
let e = {...fling.expiration}; //TODO: sharing is not cloned .catch(error => log.error(error))
let value = ev.currentTarget.value; }
if(value === "never") { return (
e = {}; <div className={`modal ${props.active ? "active" : ""}`}>
} else { <div className="modal-overlay" aria-label="Close" onClick={handleClose}></div>
e.type = value; <div className="modal-container">
e.value = ""; <div className="modal-header">
} <div className="modal-title h5">New Fling</div>
f.expiration = e;
setFling(f);
}
function setExpirationValue(ev) {
let f = {...fling};
let e = {...fling.expiration}; //TODO: sharing is not cloned
let value = e.type === "time" ? ev.currentTarget.valueAsNumber: ev.currentTarget.value;
e.value = value;
f.expiration = e;
setFling(f);
}
function formatExpirationTime() {
if (!fling.expiration || !fling.expiration.value || fling.expiration.type !== "time")
return "";
let date = new Date(fling.expiration.value);
let fmt = date.toISOString().split("T")[0];
return fmt;
}
function setAuthCode(ev) {
let f = {...fling};
let value = ev.currentTarget.value;
f.authCode = value;
setFling(f);
}
function handleSubmit(ev) {
ev.preventDefault();
log.info("Creating new filing");
log.info(fling);
flingClient.postFling(fling);
handleClose();
}
return(
<div className={`modal ${props.active ? "active": ""}`}>
<div className="modal-overlay" aria-label="Close" onClick={handleClose}></div>
<div className="modal-container">
<div className="modal-header">
<div className="modal-title h5">New Fling</div>
</div>
<div className="modal-body">
<form className="form-horizontal" onSubmit={handleSubmit}>
<div className="form-group">
<div className="col-3 col-sm-12">
<label className="form-label" htmlFor="input-name">Name</label>
</div>
<div className="col-9 col-sm-12">
<input className="form-input" type="text" id="input-name" value={fling.name} onChange={setName}/>
</div>
</div>
<div className="form-group">
<div className="col-3 col-sm-12">
<label className="form-label" htmlFor="input-share-url">Share URL</label>
</div>
<div className="col-9 col-sm-12">
<input className="form-input" type="text" id="input-share-url" value={fling.sharing.shareUrl} onChange={setShareUrl} />
<i className={`icon icon-cross text-error ${shareUrlUnique ? "d-hide": "d-visible"}`} />
</div>
</div>
<div className="form-group">
<div className="col-3 col-sm-12">
<label className="form-label" htmlFor="input-passcode">Passcode</label>
</div>
<div className="col-9 col-sm-12">
<div className="input-group">
<input className="form-input" type="text" value={fling.authCode} onChange={setAuthCode} />
<label className="form-switch ml-2 tooltip tooltip-left" data-tooltip={fling.authCode ? "Clear passcode to\ndisable protection": "Set passcode to\nenable protection"} >
<input type="checkbox" checked={!!fling.authCode} readOnly />
<i className="form-icon" /> Protected
</label>
</div>
</div>
</div>
<div className="form-group">
<div className="col-3 col-sm-12">
<label className="form-label">Expiration</label>
</div>
<div className="col-9 col-sm-12">
<div className="form-group">
<select className="form-select" value={fling.expiration.type} onChange={setExpirationType}>
<option value="never">Never</option>
<option value="time">Date</option>
<option value="clicks">Clicks</option>
</select>
</div>
<div className={fling.expiration.type === "clicks" ? "d-visible": "d-hide"}>
<div className="input-group">
<span className="input-group-addon">Expire after</span>
<input className="form-input" type="number" value={fling.expiration.value || ""} onChange={setExpirationValue} />
<span className="input-group-addon">Clicks</span>
</div>
</div>
<div className={fling.expiration.type === "time" ? "d-visible": "d-hide"}>
<div className="input-group">
<span className="input-group-addon">Expire after</span>
<input className="form-input" type="date" value={formatExpirationTime()} onChange={setExpirationValue} />
</div>
</div>
</div>
</div>
<div className="form-group">
<div className="col-3 col-sm-12">
<label className="form-label">Settings</label>
</div>
<div className="col-9 col-sm-12">
<label className="form-switch form-inline">
<input type="checkbox" id="shared" checked={fling.sharing.shared} onChange={toggleSharing}/>
<i className="form-icon" /> Shared
</label>
<label className="form-switch form-inline">
<input type="checkbox" id="allow-upload" checked={fling.sharing.allowUpload} onChange={toggleSharing}/>
<i className="form-icon" /> Uploads
</label>
<label className="form-switch form-inline">
<input type="checkbox" id="direct-download" checked={fling.sharing.directDownload} onChange={toggleSharing}/>
<i className="form-icon" /> Direct Download
</label>
</div>
</div>
<div className="float-right">
<button className="btn btn-secondary mr-2" onClick={handleClose}>Cancel</button>
<input type="submit" className="btn btn-primary" value="Save" />
</div>
</form>
</div>
</div>
</div> </div>
); <div className="modal-body">
<form className="form-horizontal" onSubmit={handleSubmit}>
<div className="form-group">
<div className="col-3 col-sm-12">
<label className="form-label" htmlFor="input-name">Name</label>
</div>
<div className="col-9 col-sm-12">
<input className="form-input" type="text" id="input-name" value={fling.name} onChange={setName} />
</div>
</div>
<div className="form-group">
<div className="col-3 col-sm-12">
<label className="form-label" htmlFor="input-share-url">Share URL</label>
</div>
<div className="col-9 col-sm-12">
<input className="form-input" type="text" id="input-share-url" value={fling.sharing.shareUrl} onChange={setShareUrl} />
<i className={`icon icon-cross text-error ${shareUrlUnique ? "d-hide" : "d-visible"}`} />
</div>
</div>
<div className="form-group">
<div className="col-3 col-sm-12">
<label className="form-label" htmlFor="input-passcode">Passcode</label>
</div>
<div className="col-9 col-sm-12">
<div className="input-group">
<input className="form-input" type="text" value={fling.authCode} onChange={setAuthCode} />
<label className="form-switch ml-2 tooltip tooltip-left" data-tooltip={fling.authCode ? "Clear passcode to\ndisable protection" : "Set passcode to\nenable protection"} >
<input type="checkbox" checked={!!fling.authCode} readOnly />
<i className="form-icon" /> Protected
</label>
</div>
</div>
</div>
<div className="form-group">
<div className="col-3 col-sm-12">
<label className="form-label">Expiration</label>
</div>
<div className="col-9 col-sm-12">
<div className="form-group">
<select className="form-select" value={fling.expiration.type} onChange={setExpirationType}>
<option value="never">Never</option>
<option value="time">Date</option>
<option value="clicks">Clicks</option>
</select>
</div>
<div className={fling.expiration.type === "clicks" ? "d-visible" : "d-hide"}>
<div className="input-group">
<span className="input-group-addon">Expire after</span>
<input className="form-input" type="number" value={fling.expiration.value || ""} onChange={setExpirationValue} />
<span className="input-group-addon">Clicks</span>
</div>
</div>
<div className={fling.expiration.type === "time" ? "d-visible" : "d-hide"}>
<div className="input-group">
<span className="input-group-addon">Expire after</span>
<input className="form-input" type="date" value={formatExpirationTime()} onChange={setExpirationValue} />
</div>
</div>
</div>
</div>
<div className="form-group">
<div className="col-3 col-sm-12">
<label className="form-label">Settings</label>
</div>
<div className="col-9 col-sm-12">
<label className="form-switch form-inline">
<input type="checkbox" id="shared" checked={fling.sharing.shared} onChange={toggleSharing} />
<i className="form-icon" /> Shared
</label>
<label className="form-switch form-inline">
<input type="checkbox" id="allow-upload" checked={fling.sharing.allowUpload} onChange={toggleSharing} />
<i className="form-icon" /> Uploads
</label>
<label className="form-switch form-inline">
<input type="checkbox" id="direct-download" checked={fling.sharing.directDownload} onChange={toggleSharing} />
<i className="form-icon" /> Direct Download
</label>
</div>
</div>
<div className="float-right">
<button className="btn btn-secondary mr-2" onClick={handleClose}>Cancel</button>
<input type="submit" className="btn btn-primary" value="Save" />
</div>
</form>
</div>
</div>
</div>
);
} }

View file

@ -1,277 +1,272 @@
import log from 'loglevel'; import log from 'loglevel';
import React, {useState, useEffect} from 'react'; import React, { useState } from 'react';
import produce from 'immer';
import {flingClient} from '../../util/flingclient'; import { useSelector, useDispatch } from 'react-redux';
import { retrieveFlings } from "../../redux/actions";
import { FlingClient } from '../../util/fc';
export default function Settings(props) { export default function Settings(props) {
let defaultState = () => ({name: "", authCode: "", let flingClient = new FlingClient();
sharing: {directDownload: false, allowUpload: true, shared: true, shareUrl: ""}, let dispatch = useDispatch();
expiration: {}});
let [fling, setFling] = useState(defaultState()); /**
let [shareUrlUnique, setShareUrlUnique] = useState(true); * The active fling from the redux store. Treat this as immutable.
let [authCodeChangeable, setAuthCodeChangeable] = useState(false); */
let [reload, setReload] = useState(true); let activeFling = useSelector(state => state.flings.activeFling);
useEffect(() => { /**
if(props.activeFling && reload) { * Deep clone the active fling from redux into a draft. Changes to the
flingClient.getFling(props.activeFling) * settings will be stored in the draft until saved and pushed to the
.then(result => { * backend. This in turn will synchronize back to the redux store.
let f = {...fling, ...result}; *
let s = {...fling.sharing, ...result.sharing}; * The draft, just as the activeFling, is of type Fling
let e = {...fling.expiration, ...result.expiration}; */
let [draft, setDraft] = useState(produce(activeFling, draft => draft));
f.sharing = s; let [shareUrlUnique, setShareUrlUnique] = useState(true);
f.expiration = e; let [authCodeChangeable, setAuthCodeChangeable] = useState(false);
setFling(f); let [expirationType, setExpirationType] = useState(activeFling.expirationClicks
? "clicks"
: activeFling.expirationTime ? "time" : "never");
setAuthCodeChangeable(!f.authCode); /**
setReload(false); * Publishes the draft to the backend and refreshes the redux store
}); */
} function publishDraft() {
}, [props.activeFling, reload, fling]); flingClient.putFling(activeFling.id, { fling: draft })
.then(
success => {
log.info("Saved new settings {}", draft);
dispatch(retrieveFlings());
})
.catch(error => log.error(`Could not save new settings for ${activeFling.id}: `, error));
}
function reloadSettings(ev) { /**
ev.preventDefault(); * Resets the draft to a new clone of the active fling. All draft
setFling(defaultState()); * modifications get lost.
setReload(true); */
} function resetDraft() {
setDraft(produce({}, draft => activeFling));
}
function resetAuthCode(ev) { /**
if(fling.authCode) { * A helper shim for persistent produce.
let f = {...fling}; *
f.authCode = ""; * Executes `fun` in immer.produce, hereby generating a new draft `newDraft`,
setFling(f); * and sets it into local state via `setDraft(newDraft)`
} */
let _pproduce = (fun) => (...args) => {
let x = produce(fun)(...args);
setDraft(x);
}
if(!ev.currentTarget.checked) { /**
setAuthCodeChangeable(true); * Sets the sharing toggles to valid combinations depending on the changed
} * setting and its new value.
} *
* Creates a new draft and sets it into the local state.
function toggleSharing(ev) { */
let f = {...fling}; let toggleSharing = _pproduce((newDraft, setting, enabled) => {
let s = {...fling.sharing}; switch (setting) {
case "direct-download":
if(ev.currentTarget.id === "direct-download") { if (enabled) {
if(ev.currentTarget.checked) { newDraft.directDownload = true;
s.directDownload = true; newDraft.shared = true;
s.shared = true; newDraft.allowUpload = false;
s.allowUpload = false;
} else {
s.directDownload = false;
}
} else if(ev.currentTarget.id === "allow-upload") {
if(ev.currentTarget.checked) {
s.allowUpload = true;
s.shared = true;
s.directDownload = false;
} else {
s.allowUpload = false;
}
} else if(ev.currentTarget.id === "shared") {
if(!ev.currentTarget.checked) {
s.allowUpload = s.directDownload = s.shared = false;
} else {
s.shared = true;
}
}
f.sharing = s;
setFling(f);
}
function setShareUrl(ev) {
let f = {...fling};
let s = {...fling.sharing}; //TODO: expiration is not cloned
let value = ev.currentTarget.value;
if(!value) {
setShareUrlUnique(false);
s.shareUrl = value;
f.sharing = s;
setFling(f);
return;
}
flingClient.getFlingByShareId(ev.currentTarget.value)
.then(result => {
if(!result) {
setShareUrlUnique(true);
} else if(props.activeFling === result.id) { // share url didn't change
setShareUrlUnique(true);
} else {
setShareUrlUnique(false);
}
s.shareUrl = value;
f.sharing = s;
setFling(f);
});
}
function setName(ev) {
let f = {...fling};
let value = ev.currentTarget.value;
f.name = value;
setFling(f);
}
function setExpirationType(ev) {
let f = {...fling};
let e = {...fling.expiration}; //TODO: sharing is not cloned
let value = ev.currentTarget.value;
if(value === "never") {
e = {};
} else { } else {
e.type = value; newDraft.directDownload = false;
e.value = "";
} }
return newDraft;
case "allow-upload":
if (enabled) {
newDraft.allowUpload = true;
newDraft.shared = true;
newDraft.directDownload = false;
} else {
newDraft.allowUpload = false;
}
return newDraft;
case "shared":
if (!enabled) {
newDraft.allowUpload = false;
newDraft.directDownload = false;
newDraft.shared = false;
} else {
newDraft.shared = true;
}
return newDraft;
default:
log.warn("Unknown action");
break;
};
})
/** Sets the Fling.name. Creates a new draft and sets it into the local state. */
let setName = _pproduce((newDraft, name) => { newDraft.name = name });
/** Sets the Fling.shareId. Creates a new draft and sets it into the local
* state. Sets `setShareUrlUnique`. */
let setShareId = _pproduce((newDraft, shareId) => {
newDraft.shareId = shareId;
f.expiration = e; flingClient.getFlingByShareId(shareId)
setFling(f); .then(result => shareId !== activeFling.shareId && setShareUrlUnique(false))
.catch(error => error.status === 404 && setShareUrlUnique(true));
});
/** Sets the Fling.authCode. Creates a new draft and sets it into the local state. */
let setAuthCode = _pproduce((newDraft, authCode) => {
setAuthCodeChangeable(true);
if (!authCode) return newDraft;
newDraft.authCode = authCode
});
let resetAuthCode = _pproduce((newDraft) => {
setAuthCodeChangeable(true);
newDraft.authCode = "";
return newDraft;
});
let setExpiration = _pproduce((newDraft, type, value) => {
setExpirationType(type)
switch (type) {
case "clicks":
newDraft.expirationTime = "";
newDraft.expirationClicks = value;
break;
case "time":
newDraft.expirationClicks = "";
newDraft.expirationTime = value;
break;
case "never":
newDraft.expirationClicks = "";
newDraft.expirationTime = "";
break;
default:
log.error("Unknown expiration type");
break;
} }
});
function setExpirationValue(ev) {
let f = {...fling};
let e = {...fling.expiration}; //TODO: sharing is not cloned
let value = e.type === "time" ? ev.currentTarget.valueAsNumber: ev.currentTarget.value;
e.value = value;
f.expiration = e;
setFling(f);
}
function formatExpirationTime() {
if (!fling.expiration || !fling.expiration.value || fling.expiration.type !== "time")
return "";
let date = new Date(fling.expiration.value); let resetExpiration = (draft, type) => {
let fmt = date.toISOString().split("T")[0]; setExpiration(draft, type, "");
return fmt; };
}
function setAuthCode(ev) { return (
let f = {...fling}; <div className="container">
let value = ev.currentTarget.value; <div className="columns">
<div className="p-centered column col-xl-9 col-sm-12 col-6">
f.authCode = value; <form className="form-horizontal" onSubmit={(ev) => { ev.preventDefault(); publishDraft(); }}>
setFling(f); <div className="form-group">
} <div className="col-3 col-sm-12">
<label className="form-label" htmlFor="input-name">Name</label>
function handleSubmit(ev) { </div>
ev.preventDefault(); <div className="col-9 col-sm-12">
log.info(fling); <input className="form-input" type="text" id="input-name"
flingClient.putFling(props.activeFling, fling); value={draft.name}
} onChange={(ev) => setName(draft, ev.target.value)} />
</div>
return(
<div className="container">
<div className="columns">
<div className="p-centered column col-xl-9 col-sm-12 col-6">
<form className="form-horizontal" onSubmit={handleSubmit}>
<div className="form-group">
<div className="col-3 col-sm-12">
<label className="form-label" htmlFor="input-name">Name</label>
</div>
<div className="col-9 col-sm-12">
<input className="form-input" type="text" id="input-name" value={fling.name} onChange={setName}/>
</div>
</div>
<div className="form-group">
<div className="col-3 col-sm-12">
<label className="form-label" htmlFor="input-share-url">Share URL</label>
</div>
<div className="col-9 col-sm-12">
<input className="form-input" type="text" id="input-share-url" value={fling.sharing.shareUrl} onChange={setShareUrl} />
<i className={`icon icon-cross text-error ${shareUrlUnique ? "d-hide": "d-visible"}`} />
</div>
</div>
<div className="form-group">
<div className="col-3 col-sm-12">
<label className="form-label" htmlFor="input-passcode">Passcode</label>
</div>
<div className="col-9 col-sm-12">
<div className="input-group">
<input className={`form-input ${authCodeChangeable ? "d-visible":"d-hide"}`} type="text" readOnly={!authCodeChangeable} value={fling.authCode} onChange={setAuthCode} />
<label className="form-switch ml-2 popover popover-bottom">
<input type="checkbox" checked={!!fling.authCode} onChange={resetAuthCode} />
<i className="form-icon" /> Protected
<div className="popover-container card">
<div className="card-body">
{fling.authCode ? "Click to reset passcode": "Set passcode to enable protection"}
</div>
</div>
</label>
</div>
</div>
</div>
<div className="form-group">
<div className="col-3 col-sm-12">
<label className="form-label">Expiration</label>
</div>
<div className="col-9 col-sm-12">
<div className="form-group">
<select className="form-select" value={fling.expiration.type} onChange={setExpirationType}>
<option value="never">Never</option>
<option value="time">Date</option>
<option value="clicks">Clicks</option>
</select>
</div>
<div className={fling.expiration.type === "clicks" ? "d-visible": "d-hide"}>
<div className="input-group">
<span className="input-group-addon">Expire after</span>
<input className="form-input" type="number" value={fling.expiration.value || ""} onChange={setExpirationValue} />
<span className="input-group-addon">Clicks</span>
</div>
</div>
<div className={fling.expiration.type === "time" ? "d-visible": "d-hide"}>
<div className="input-group">
<span className="input-group-addon">Expire after</span>
<input className="form-input" type="date" value={formatExpirationTime()} onChange={setExpirationValue} />
</div>
</div>
</div>
</div>
<div className="form-group">
<div className="col-3 col-sm-12">
<label className="form-label">Settings</label>
</div>
<div className="col-9 col-sm-12">
<label className="form-switch form-inline">
<input type="checkbox" id="shared" checked={fling.sharing.shared} onChange={toggleSharing}/>
<i className="form-icon" /> Shared
</label>
<label className="form-switch form-inline">
<input type="checkbox" id="allow-upload" checked={fling.sharing.allowUpload} onChange={toggleSharing}/>
<i className="form-icon" /> Uploads
</label>
<label className="form-switch form-inline">
<input type="checkbox" id="direct-download" checked={fling.sharing.directDownload} onChange={toggleSharing}/>
<i className="form-icon" /> Direct Download
</label>
</div>
</div>
<div className="float-right">
<button className="btn btn-secondary mr-2" onClick={reloadSettings}>Cancel</button>
<input type="submit" className="btn btn-primary" value="Save" />
</div>
</form>
</div> </div>
</div> <div className="form-group">
<div className="col-3 col-sm-12">
<label className="form-label" htmlFor="input-share-url">Share Id</label>
</div>
<div className="col-9 col-sm-12">
<input className="form-input" type="text" id="input-share-url"
value={draft.shareId}
onChange={ev => setShareId(draft, ev.target.value)} />
<i className={`icon icon-cross text-error ${shareUrlUnique ? "d-hide" : "d-visible"}`} />
</div>
</div>
<div className="form-group">
<div className="col-3 col-sm-12">
<label className="form-label" htmlFor="input-passcode">Passcode</label>
</div>
<div className="col-9 col-sm-12">
<div className="input-group">
<input className={`form-input ${!draft.authCode || authCodeChangeable ? "d-visible" : "d-hide"}`} type="text"
value={draft.authCode}
onChange={ev => setAuthCode(draft, ev.target.value)} />
<label className="form-switch ml-2 popover popover-bottom">
<input type="checkbox" checked={!!draft.authCode} onChange={ev => resetAuthCode(draft)} />
<i className="form-icon" /> Protected
<div className="popover-container card">
<div className="card-body">
{draft.authCode ? "Click to reset passcode" : "Set passcode to enable protection"}
</div>
</div>
</label>
</div>
</div>
</div>
<div className="form-group">
<div className="col-3 col-sm-12">
<label className="form-label">Expiration</label>
</div>
<div className="col-9 col-sm-12">
<div className="form-group">
<select className="form-select" value={expirationType} onChange={ev => resetExpiration(draft, ev.currentTarget.value)}>
<option value="never">Never</option>
<option value="time">Date</option>
<option value="clicks">Clicks</option>
</select>
</div>
<div className={expirationType === "clicks" ? "d-visible" : "d-hide"}>
<div className="input-group">
<span className="input-group-addon">Expire after</span>
<input className="form-input" type="number" value={draft.expirationClicks} onChange={ev => setExpiration(draft, "clicks", ev.target.value)} />
<span className="input-group-addon">Clicks</span>
</div>
</div>
<div className={expirationType === "time" ? "d-visible" : "d-hide"}>
<div className="input-group">
<span className="input-group-addon">Expire after</span>
<input className="form-input" type="date"
value={draft.expirationTime ? (new Date(draft.expirationTime)).toISOString().split('T')[0]: ""}
onChange={ev => setExpiration(draft, "time", ev.target.valueAsNumber)} />
</div>
</div>
</div>
</div>
<div className="form-group">
<div className="col-3 col-sm-12">
<label className="form-label">Settings</label>
</div>
<div className="col-9 col-sm-12">
<label className="form-switch form-inline">
<input type="checkbox" id="shared" checked={draft.shared}
onChange={ev => toggleSharing(draft, ev.target.id, ev.target.checked)} />
<i className="form-icon" /> Shared
</label>
<label className="form-switch form-inline">
<input type="checkbox" id="allow-upload" checked={draft.allowUpload}
onChange={ev => toggleSharing(draft, ev.target.id, ev.target.checked)} />
<i className="form-icon" /> Uploads
</label>
<label className="form-switch form-inline">
<input type="checkbox" id="direct-download" checked={draft.directDownload}
onChange={ev => toggleSharing(draft, ev.target.id, ev.target.checked)} />
<i className="form-icon" /> Direct Download
</label>
</div>
</div>
<div className="float-right">
<button className="btn btn-secondary mr-2" onClick={resetDraft}>Cancel</button>
<input type="submit" className="btn btn-primary" value="Save" />
</div>
</form>
</div> </div>
); </div>
</div>
);
} }

View file

@ -1,217 +1,208 @@
import log from 'loglevel'; import log from 'loglevel';
import React, {useState, useEffect, useRef} from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { useSelector } from "react-redux";
import {artifactClient} from '../../util/flingclient'; import { ArtifactClient, FlingClient, fc } from '../../util/fc';
import { prettifyBytes, prettifyTimestamp } from '../../util/fn';
import upload from '../resources/upload.svg'; import upload from '../resources/upload.svg';
import drop from '../resources/drop.svg'; import drop from '../resources/drop.svg';
export default function Upload() {
let fileInputRef = useRef(null);
let [files, setFiles] = useState([]);
let [dragging, setDragging] = useState(false);
let [dragCount, setDragCount] = useState(0);
export default function Upload(props) { const activeFling = useSelector(state => state.flings.activeFling);
let fileInputRef = useRef(null);
let [files, setFiles] = useState([]);
let [dragging, setDragging] = useState(false);
let [dragCount, setDragCount] = useState(0);
useEffect(() => { useEffect(() => {
// prevent browser from trying to open the file when drag event // prevent browser from trying to open the file when drag event not
// not recognized properly // recognized properly
window.addEventListener("dragover",function(e){ window.addEventListener("dragover", e => e.preventDefault(), false);
e.preventDefault(); window.addEventListener("drop", e => e.preventDefault(), false);
},false); });
window.addEventListener("drop",function(e){
e.preventDefault();
},false);
});
function fileList() { function fileList() {
function readableBytes(bytes) { let fileList = [];
if(bytes <= 0) return "0 KB"; files.forEach((file, idx) => {
if (!file.uploaded) {
var i = Math.floor(Math.log(bytes) / Math.log(1024)), fileList.push(
sizes = ['Byte', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; <div key={idx} className="column col-6 col-md-12 mb-2">
<div className="card">
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i]; <div className="card-header">
} <i className="icon icon-cross float-right c-hand"
onClick={ev => deleteFile(idx)} />
let fileList = []; <div className="card-title h5">{file.name}</div>
files.forEach((file,idx) => { <div className="card-subtitle text-gray">
if(!file.uploaded) { {`${prettifyTimestamp(file.lastModified)}, ` +
fileList.push( `${prettifyBytes(file.size)}`}
<div className="column col-6 col-md-12 mb-2">
<div className="card">
<div className="card-header">
<i className="icon icon-cross float-right c-hand" onClick={ev => deleteFile(idx)}/>
<div className="card-title h5">{file.name}</div>
<div className="card-subtitle text-gray">{(new Date(file.lastModified)).toLocaleString()+", "+readableBytes(file.size)}</div>
</div>
</div>
</div>
);
}
});
return fileList;
}
function deleteFile(idx) {
let f = [...files];
f.splice(idx, 1);
setFiles(f);
}
function totalSize() {
function readableBytes(bytes) {
if(bytes <= 0) return "0 KB";
var i = Math.floor(Math.log(bytes) / Math.log(1024)),
sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i];
}
let totalSize = 0;
for(let file of files) {
totalSize += file.size;
}
return readableBytes(totalSize);
}
function handleClick(ev) {
fileInputRef.current.click();
}
function handleFileInputChange(ev) {
let fileInputFiles = fileInputRef.current.files;
if (!fileInputFiles) {
console.warn("No files selected");
return;
}
setFiles([...files, ...fileInputFiles]);
}
function handleDrop(ev) {
stopEvent(ev);
ev.persist();
let evFiles = ev.dataTransfer.files;
if (!evFiles) {
console.warn("Dropzone triggered without files");
return;
}
setFiles([...files, ...fileListToArray(evFiles)]);
setDragging(false);
setDragCount(0);
}
function fileListToArray(fileList) {
if(fileList === undefined || fileList === null) {
return [];
}
let arr = [];
for (let i=0; i<fileList.length; i++) { arr.push(fileList[i]); }
return arr;
}
function handleOnDragEnter(ev) {
stopEvent(ev);
if(dragCount === 0) setDragging(true);
setDragCount(dragCount+1);
}
function handleOnDragLeave(ev) {
stopEvent(ev);
let dc = dragCount;
dc -= 1;
setDragCount(dc);
if(dc === 0) setDragging(false);
}
function stopEvent(ev) {
ev.preventDefault();
ev.stopPropagation();
}
function logFiles() {
log.info("Files so far: ["+files.map((i) => i.name).join(',')+"]");
}
function setFileUploaded(idx) {
let f = [...files];
f[idx].uploaded = true;
setFiles(f);
}
function handleUpload() {
files.forEach((file, idx) => {
artifactClient.postArtifact(props.activeFling, file)
.then(response => {
setFileUploaded(idx);
});
});
}
function zoneContent(dragging) {
if(dragging){
return(
<>
<img className="dropzone-icon" alt="dropzone icon" src={drop} />
<h5 className="text-primary">Drop now!</h5>
</>
);
}else {
return(
<>
<img className="dropzone-icon-upload" alt="dropzone icon" src={upload} />
<h5>Click or Drop</h5>
</>
);
}
}
return(
<div className="container">
{logFiles()}
<div className="columns">
<div className="column col-4 col-sm-12">
<div className="dropzone c-hand py-2"
onDrop={handleDrop}
onClick={handleClick}
onDragOver={stopEvent}
onDragEnter={handleOnDragEnter}
onDragLeave={handleOnDragLeave}>
<input className="d-hide" ref={fileInputRef} type="file" multiple onChange={handleFileInputChange} />
{zoneContent(dragging)}
</div>
</div>
<div className="column col-8 col-sm-12" >
<div className="file-list">
<div className="row">
<div className="container">
<div className="columns">
{fileList()}
</div>
</div>
</div>
<div className="upload-command-line m-2">
<span className="total-upload">Total Size: {totalSize()}</span>
<button className="btn btn-primary btn-upload" onClick={handleUpload}>Upload</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
);
}
});
return fileList;
}
function deleteFile(idx) {
let f = [...files];
f.splice(idx, 1);
setFiles(f);
}
function totalSize() {
let totalSize = 0;
for (let file of files) {
totalSize += file.size;
}
return prettifyBytes(totalSize);
}
function handleClick(ev) {
fileInputRef.current.click();
}
function handleFileInputChange(ev) {
let fileInputFiles = fileInputRef.current.files;
if (!fileInputFiles) {
console.warn("No files selected");
return;
}
setFiles([...files, ...fileInputFiles]);
}
function handleDrop(ev) {
stopEvent(ev);
ev.persist();
let evFiles = ev.dataTransfer.files;
if (!evFiles) {
console.warn("Dropzone triggered without files");
return;
}
setFiles([...files, ...fileListToArray(evFiles)]);
setDragging(false);
setDragCount(0);
}
function fileListToArray(fileList) {
if (fileList === undefined || fileList === null) {
return [];
}
let arr = [];
for (let i = 0; i < fileList.length; i++) { arr.push(fileList[i]); }
return arr;
}
function handleOnDragEnter(ev) {
stopEvent(ev);
if (dragCount === 0) setDragging(true);
setDragCount(dragCount + 1);
}
function handleOnDragLeave(ev) {
stopEvent(ev);
let dc = dragCount;
dc -= 1;
setDragCount(dc);
if (dc === 0) setDragging(false);
}
function stopEvent(ev) {
ev.preventDefault();
ev.stopPropagation();
}
function logFiles() {
log.info("Files so far: [" + files.map((i) => i.name).join(',') + "]");
}
function setFileUploaded(idx) {
let f = [...files];
f[idx].uploaded = true;
setFiles(f);
}
function handleUpload() {
const flingClient = new FlingClient();
const artifactClient = new ArtifactClient();
files.forEach((file, idx) => {
let artifact = new fc.Artifact(file.name)
flingClient.postArtifact(activeFling.id, { artifact: artifact })
.then(artifact => {
artifactClient.uploadArtifactData(artifact.id, { body: file });
setFileUploaded(idx);
});
});
}
function zoneContent(dragging) {
if (dragging) {
return (
<>
<img className="dropzone-icon" alt="dropzone icon" src={drop} />
<h5 className="text-primary">Drop now!</h5>
</>
);
} else {
return (
<>
<img className="dropzone-icon-upload" alt="dropzone icon" src={upload} />
<h5>Click or Drop</h5>
</>
);
}
}
return (
<div className="container">
{logFiles()}
<div className="columns">
<div className="column col-4 col-sm-12">
<div className="dropzone c-hand py-2"
onDrop={handleDrop}
onClick={handleClick}
onDragOver={stopEvent}
onDragEnter={handleOnDragEnter}
onDragLeave={handleOnDragLeave}>
<input className="d-hide" ref={fileInputRef} type="file" multiple onChange={handleFileInputChange} />
{zoneContent(dragging)}
</div>
</div> </div>
);
<div className="column col-8 col-sm-12" >
<div className="file-list">
<div className="row">
<div className="container">
<div className="columns">
{fileList()}
</div>
</div>
</div>
<div className="upload-command-line m-2">
<span className="total-upload">Total Size: {totalSize()}</span>
<button className="btn btn-primary btn-upload" onClick={handleUpload}>Upload</button>
</div>
</div>
</div>
</div>
</div>
);
} }

View file

@ -1,54 +1,82 @@
import React, {useRef, useState, useEffect} from 'react'; import log from 'loglevel';
import React, { useRef, useState, useEffect } from 'react';
import {flingClient} from '../../util/flingclient'; import { AuthClient } from '../../util/fc';
export default function FlingUser(props) { export default function FlingUser(props) {
let iframeContainer = useRef(null); let iframeContainer = useRef(null);
let [packaging, setPackaging] = useState(true); let [packaging, setPackaging] = useState(true);
let [waitingMessage, setWaitingMessage] = useState(""); let [done, setDone] = useState(false);
let [downloadUrl, setDownloadUrl] = useState(""); let [waitingMessage, setWaitingMessage] = useState("");
let [downloadUrl, setDownloadUrl] = useState("");
useEffect(handleDownload, []); useEffect(handleDownload, []);
function handleDownload() { function handleDownload() {
flingClient.packageFling(props.fling.id) let authClient = new AuthClient();
.then(downloadUrl => { authClient.deriveToken({ singleUse: true })
setPackaging(false); .then(token => {
// We need this iframe hack because with a regular href, while let url = `${process.env.REACT_APP_API.replace(/\/+$/, '')}/api/fling/${props.fling.id}/data?derivedToken=${token}`;
// the browser downloads the file fine, it also reloads the page, hence log.trace(`Generated download url for link: ${url}`);
// loosing all logs and state setDownloadUrl(url);
let frame = document.createElement("iframe"); })
frame.src = downloadUrl; .then(
iframeContainer.current.appendChild(frame); authClient.deriveToken({ singleUse: true })
setDownloadUrl(downloadUrl); .then(token => {
}); setPackaging(false);
// We need this iframe hack because with a regular href, while
// the browser downloads the file fine, it also reloads the page, hence
// loosing all logs and state
let frame = document.createElement("iframe");
let url = `${process.env.REACT_APP_API.replace(/\/+$/, '')}/api/fling/${props.fling.id}/data?derivedToken=${token}`;
log.trace(`Generated download url: ${url}`);
frame.src = url;
iframeContainer.current.appendChild(frame);
}));
let randMsg = ["Please stay patient...", let randMsg = ["Please stay patient...",
"Your download will be ready soon...", "Your download will be ready soon...",
"Packaging up your files...", "Packaging up your files...",
"Almost there..."]; "Almost there..."];
setInterval(() => setWaitingMessage(randMsg[Math.floor(Math.random() * randMsg.length)]), 10000); setInterval(() => setWaitingMessage(randMsg[Math.floor(Math.random() * randMsg.length)]), 10000);
} }
return( function invalidateLink(ev) {
<div> setDone(true);
<div className="container-center"> window.location.href = downloadUrl;
<div className="card direct-download-card"> }
<div className="card-body ">
{packaging function reloadPage(ev) {
? <><div className="loading loading-lg" /> window.location.reload();
{waitingMessage ? waitingMessage: "Packaging up your files..."} }
</>
: <> return (
<h5>Your download is <span className="text-primary">ready!</span></h5> <div>
<i className="icon icon-check icon-2x text-primary" /><br/> <div className="container-center">
<span className="text-dark">Download doesn't start? <br/><a href={downloadUrl}>Click here</a></span> <div className="card direct-download-card">
</> <div className="card-body ">
} {packaging
</div> ? <><div className="loading loading-lg" />
</div> {waitingMessage ? waitingMessage : "Packaging up your files..."}
</>
: !done
? <>
<h5>Your download is <span className="text-primary">ready!</span></h5>
<i className="icon icon-check icon-2x text-primary" /><br />
<span className="text-dark">Download doesn't start? <br />
<button className="btn btn-link" onClick={invalidateLink}>Click here</button></span>
</>
: <>
<h5>Thanks for <span className="text-primary">downloading!</span></h5>
<i className="icon icon-check icon-2x text-primary" /><br />
<span className="text-dark">Want to download again? <br />
<button className="btn btn-link" onClick={reloadPage}>Reload page</button></span>
</>
}
</div> </div>
<div className="d-hide" ref={iframeContainer} />
</div> </div>
); </div>
<div className="d-hide" ref={iframeContainer} />
</div>
);
} }

View file

@ -1,26 +0,0 @@
import React, {useState} from 'react';
import classNames from 'classnames';
import log from 'loglevel';
export default (props) => {
function renderError() {
return (
<div className="toast toast-error mb-2">
<button className="btn btn-clear float-right" onClick={props.clearErrors}></button>
<h5>Ooops!</h5>
<li>
{ props.errors.map( (err, idx) => <ul key={idx}>{err}</ul> ) }
</li>
</div>
);
}
return (
<>
{ props.errors.length > 0 && !props.below ? renderError() : "" }
{ props.children }
{ props.errors.length > 0 && props.below ? renderError() : "" }
</>
);
}

View file

@ -1,26 +1,36 @@
import React, {useState, useEffect} from 'react'; import React, { useState, useEffect } from 'react';
import {useParams} from 'react-router-dom'; import { useParams } from 'react-router-dom';
import {flingClient} from '../../util/flingclient'; import { FlingClient } from '../../util/fc';
import DirectDownload from './DirectDownload'; import DirectDownload from './DirectDownload';
import FlingUserList from './FlingUserList'; import FlingUserList from './FlingUserList';
export default function FlingUser() { export default function FlingUser() {
let { shareId } = useParams(); let { shareId } = useParams();
let [fling, setFling] = useState({}); let [fling, setFling] = useState({});
let [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
flingClient.getFlingByShareId(shareId) let flingClient = new FlingClient();
.then(f => setFling(f)); flingClient.getFlingByShareId(shareId)
}, [shareId]); .then(f => {
setFling(f);
setLoading(false);
});
}, [shareId]);
return( return (
<div> <>
{fling.sharing && fling.sharing.directDownload {loading
? <DirectDownload fling={fling} /> ? <div></div>
: <FlingUserList fling={fling} />} : <div>
</div> {fling.shared && fling.directDownload
); ? <DirectDownload fling={fling} />
: <FlingUserList fling={fling} />}
</div>
}
</>
);
} }

View file

@ -1,380 +1,341 @@
import log from 'loglevel'; import log from 'loglevel';
import React, {useState, useEffect, useRef} from 'react'; import React, { useState, useEffect, useRef } from 'react';
import {Switch, Route, useLocation, Link} from "react-router-dom"; import { Switch, Route, useLocation, Link } from "react-router-dom";
import {flingClient, artifactClient} from '../../util/flingclient'; import { FlingClient, AuthClient, ArtifactClient, fc } from '../../util/fc';
import { prettifyTimestamp, prettifyBytes } from '../../util/fn';
import upload from '../resources/upload.svg'; import upload from '../resources/upload.svg';
import drop from '../resources/drop.svg'; import drop from '../resources/drop.svg';
function Artifacts(props) { function Artifacts(props) {
let [artifacts, setArtifacts] = useState([]); let [artifacts, setArtifacts] = useState([]);
useEffect(() => { useEffect(() => {
if(!props.fling) return; if (!props.fling) return;
artifactClient.getArtifacts(props.fling.id) let flingClient = new FlingClient();
.then((artifacts) => setArtifacts(artifacts)); flingClient.getArtifacts(props.fling.id)
}, [props.fling]); .then(artifacts => setArtifacts(artifacts));
}, [props.fling]);
function renderArtifact(artifact) { function renderArtifact(artifact) {
function readableBytes(bytes) {
if(bytes <= 0) return "0 KB";
var i = Math.floor(Math.log(bytes) / Math.log(1024)),
sizes = ['Byte', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i];
}
function localizedDate(t) {
let d = new Date(t);
return d.toLocaleDateString();
}
return(
<div className="user-list-artifact">
<div className="container">
<div className="columns">
<div className="column col-8 col-sm-12">
{artifact.name}<br />
</div>
<div className="column col-2 col-sm-6">
<div className="text-gray">{readableBytes(artifact.size)}</div>
</div>
<div className="column col-2 col-sm-6">
<div className="text-gray float-right">{localizedDate(artifact.uploadTime)}</div>
</div>
</div>
</div>
</div>
);
}
return ( return (
<div> <div className="user-list-artifact">
{artifacts.map(renderArtifact)} <div className="container">
<div className="columns">
<div className="column col-8 col-sm-12">
{artifact.path}<br />
</div>
<div className="column col-2 col-sm-6">
<div className="text-gray"></div>
</div>
<div className="column col-2 col-sm-6">
<div className="text-gray float-right">{prettifyTimestamp(artifact.creationTime)}</div>
</div>
</div>
</div> </div>
</div>
); );
}
return (
<div>
{artifacts.map(renderArtifact)}
</div>
);
} }
function Upload(props) { function Upload(props) {
let fileInputRef = useRef(null); let fileInputRef = useRef(null);
let [files, setFiles] = useState([]); let [files, setFiles] = useState([]);
let [dragging, setDragging] = useState(false); let [dragging, setDragging] = useState(false);
let [dragCount, setDragCount] = useState(0); let [dragCount, setDragCount] = useState(0);
useEffect(() => { useEffect(() => {
// prevent browser from trying to open the file when drag event // prevent browser from trying to open the file when drag event
// not recognized properly // not recognized properly
window.addEventListener("dragover",function(e){ window.addEventListener("dragover", function(e) {
e.preventDefault(); e.preventDefault();
},false); }, false);
window.addEventListener("drop",function(e){ window.addEventListener("drop", function(e) {
e.preventDefault(); e.preventDefault();
},false); }, false);
}); });
function fileList() {
function readableBytes(bytes) {
if(bytes <= 0) return "0 KB";
var i = Math.floor(Math.log(bytes) / Math.log(1024)),
sizes = ['Byte', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i];
}
let fileList = [];
files.forEach((file,idx) => {
if(!file.uploaded) {
fileList.push(
<div className="column col-6 col-md-12 mb-2">
<div className="card">
<div className="card-header">
<i className="icon icon-cross float-right c-hand" onClick={ev => deleteFile(idx)}/>
<div className="card-title h5">{file.name}</div>
<div className="card-subtitle text-gray">{(new Date(file.lastModified)).toLocaleString()+", "+readableBytes(file.size)}</div>
</div>
</div>
</div>
);
}
});
return fileList;
}
function deleteFile(idx) {
let f = [...files];
f.splice(idx, 1);
setFiles(f);
}
function totalSize() {
function readableBytes(bytes) {
if(bytes <= 0) return "0 KB";
var i = Math.floor(Math.log(bytes) / Math.log(1024)),
sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i];
}
let totalSize = 0;
for(let file of files) {
totalSize += file.size;
}
return readableBytes(totalSize);
}
function handleClick(ev) {
fileInputRef.current.click();
}
function handleFileInputChange(ev) {
let fileInputFiles = fileInputRef.current.files;
if (!fileInputFiles) {
console.warn("No files selected");
return;
}
setFiles([...files, ...fileInputFiles]);
}
function handleDrop(ev) {
stopEvent(ev);
ev.persist();
let evFiles = ev.dataTransfer.files;
if (!evFiles) {
console.warn("Dropzone triggered without files");
return;
}
setFiles([...files, ...fileListToArray(evFiles)]);
setDragging(false);
setDragCount(0);
}
function fileListToArray(fileList) {
if(fileList === undefined || fileList === null) {
return [];
}
let arr = [];
for (let i=0; i<fileList.length; i++) { arr.push(fileList[i]); }
return arr;
}
function handleOnDragEnter(ev) {
stopEvent(ev);
if(dragCount === 0) setDragging(true);
setDragCount(dragCount+1);
}
function handleOnDragLeave(ev) {
stopEvent(ev);
let dc = dragCount;
dc -= 1;
setDragCount(dc);
if(dc === 0) setDragging(false);
}
function stopEvent(ev) {
ev.preventDefault();
ev.stopPropagation();
}
function logFiles() {
log.info("Files so far: ["+files.map((i) => i.name).join(',')+"]");
}
function setFileUploaded(idx) {
let f = [...files];
f[idx].uploaded = true;
setFiles(f);
}
function handleUpload() {
files.forEach((file, idx) => {
artifactClient.postArtifact(props.fling.id, file)
.then(response => {
setFileUploaded(idx);
});
});
}
function zoneContent(dragging) {
if(dragging){
return(
<>
<img className="dropzone-icon" alt="dropzone icon" src={drop} />
<h5 className="text-primary">Drop now!</h5>
</>
);
}else {
return(
<>
<img className="dropzone-icon-upload" alt="dropzone icon" src={upload} />
<h5>Click or Drop</h5>
</>
);
}
}
return(
<div className="container">
{logFiles()}
<div className="columns">
<div className="column col-4 col-sm-12">
<div className="dropzone c-hand py-2"
onDrop={handleDrop}
onClick={handleClick}
onDragOver={stopEvent}
onDragEnter={handleOnDragEnter}
onDragLeave={handleOnDragLeave}>
<input className="d-hide" ref={fileInputRef} type="file" multiple onChange={handleFileInputChange} />
{zoneContent(dragging)}
function fileList() {
let fileList = [];
files.forEach((file, idx) => {
if (!file.uploaded) {
fileList.push(
<div className="column col-6 col-md-12 mb-2">
<div className="card">
<div className="card-header">
<i className="icon icon-cross float-right c-hand" onClick={ev => deleteFile(idx)} />
<div className="card-title h5">{file.name}</div>
<div className="card-subtitle text-gray">{(new Date(file.lastModified)).toLocaleString() + ", " + prettifyBytes(file.size)}</div>
</div> </div>
</div> </div>
</div>
);
}
});
<div className="column col-8 col-sm-12" > return fileList;
<div className="file-list"> }
<div className="row">
<div className="container"> function deleteFile(idx) {
<div className="columns"> let f = [...files];
{fileList()} f.splice(idx, 1);
</div> setFiles(f);
</div> }
</div>
<div className="upload-command-line m-2"> function totalSize() {
<span className="total-upload">Total Size: {totalSize()}</span> let totalSize = 0;
<button className="btn btn-primary btn-upload" onClick={handleUpload}>Upload</button> for (let file of files) {
totalSize += file.size;
}
return prettifyBytes(totalSize);
}
function handleClick(ev) {
fileInputRef.current.click();
}
function handleFileInputChange(ev) {
let fileInputFiles = fileInputRef.current.files;
if (!fileInputFiles) {
console.warn("No files selected");
return;
}
setFiles([...files, ...fileInputFiles]);
}
function handleDrop(ev) {
stopEvent(ev);
ev.persist();
let evFiles = ev.dataTransfer.files;
if (!evFiles) {
console.warn("Dropzone triggered without files");
return;
}
setFiles([...files, ...fileListToArray(evFiles)]);
setDragging(false);
setDragCount(0);
}
function fileListToArray(fileList) {
if (fileList === undefined || fileList === null) {
return [];
}
let arr = [];
for (let i = 0; i < fileList.length; i++) { arr.push(fileList[i]); }
return arr;
}
function handleOnDragEnter(ev) {
stopEvent(ev);
if (dragCount === 0) setDragging(true);
setDragCount(dragCount + 1);
}
function handleOnDragLeave(ev) {
stopEvent(ev);
let dc = dragCount;
dc -= 1;
setDragCount(dc);
if (dc === 0) setDragging(false);
}
function stopEvent(ev) {
ev.preventDefault();
ev.stopPropagation();
}
function logFiles() {
log.info("Files so far: [" + files.map((i) => i.name).join(',') + "]");
}
function setFileUploaded(idx) {
let f = [...files];
f[idx].uploaded = true;
setFiles(f);
}
function handleUpload() {
const flingClient = new FlingClient();
const artifactClient = new ArtifactClient();
files.forEach((file, idx) => {
let artifact = new fc.Artifact(file.name)
flingClient.postArtifact(props.fling.id, { artifact: artifact })
.then(artifact => {
artifactClient.uploadArtifactData(artifact.id, { body: file });
setFileUploaded(idx);
});
});
}
function zoneContent(dragging) {
if (dragging) {
return (
<>
<img className="dropzone-icon" alt="dropzone icon" src={drop} />
<h5 className="text-primary">Drop now!</h5>
</>
);
} else {
return (
<>
<img className="dropzone-icon-upload" alt="dropzone icon" src={upload} />
<h5>Click or Drop</h5>
</>
);
}
}
return (
<div className="container">
{logFiles()}
<div className="columns">
<div className="column col-4 col-sm-12">
<div className="dropzone c-hand py-2"
onDrop={handleDrop}
onClick={handleClick}
onDragOver={stopEvent}
onDragEnter={handleOnDragEnter}
onDragLeave={handleOnDragLeave}>
<input className="d-hide" ref={fileInputRef} type="file" multiple onChange={handleFileInputChange} />
{zoneContent(dragging)}
</div>
</div>
<div className="column col-8 col-sm-12" >
<div className="file-list">
<div className="row">
<div className="container">
<div className="columns">
{fileList()}
</div> </div>
</div> </div>
</div> </div>
<div className="upload-command-line m-2">
<span className="total-upload">Total Size: {totalSize()}</span>
<button className="btn btn-primary btn-upload" onClick={handleUpload}>Upload</button>
</div>
</div> </div>
</div> </div>
); </div>
</div>
);
} }
export default function FlingUserList(props) { export default function FlingUserList(props) {
let location = useLocation(); let location = useLocation();
let iframeContainer = useRef(null); let iframeContainer = useRef(null);
let [infoText, setInfoText] = useState(""); let [infoText, setInfoText] = useState("");
let [inProgress, setInProgress] = useState(false); let [inProgress, setInProgress] = useState(false);
useEffect((flingId) => { useEffect(() => {
if(!flingId) return; if (!props.fling.id) return;
function readableBytes(bytes) { let flingClient = new FlingClient();
if(bytes <= 0) return "0 KB"; flingClient.getArtifacts(props.fling.id)
.then((artifacts) => {
setInfoText(`${prettifyTimestamp(props.fling.creationTime)} - ${artifacts.length} files`);
});
}, [props.fling.id, props.fling.creationTime]);
var i = Math.floor(Math.log(bytes) / Math.log(1024)), function handleDownload(ev) {
sizes = ['Byte', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; ev.preventDefault();
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i]; setInProgress(true);
} let authClient = new AuthClient();
authClient.deriveToken({ singleUse: true })
.then(token => {
// We need this iframe hack because with a regular href, while
// the browser downloads the file fine, it also reloads the page, hence
// loosing all logs and state
let frame = document.createElement("iframe");
let url = `${process.env.REACT_APP_API.replace(/\/+$/, '')}/api/fling/${props.fling.id}/data?derivedToken=${token}`;
log.trace(`Generated download url: ${url}`);
frame.src = url;
setInProgress(false);
iframeContainer.current.appendChild(frame);
});
}
function localizedDate(t) { function path(tail) {
let d = new Date(t); if (props.fling && props.fling.shareId) {
return d.toLocaleDateString(); return `/f/${props.fling.shareId}/${tail}`;
}
artifactClient.getArtifacts(flingId)
.then((artifacts) => {
let totalSize = 0;
let countArtifacts = 0;
for(let artifact of artifacts) {
totalSize += artifact.size;
countArtifacts++;
}
setInfoText(`${localizedDate(props.fling.creationTime)} - ${countArtifacts} files - ${readableBytes(totalSize)}`);
});
}, [props.fling.id, props.fling.creationTime]);
function handleDownload(ev) {
ev.preventDefault();
setInProgress(true);
flingClient.packageFling(props.fling.id)
.then(downloadUrl => {
// We need this iframe hack because with a regular href, while
// the browser downloads the file fine, it also reloads the page, hence
// loosing all logs and state
let frame = document.createElement("iframe");
frame.src = downloadUrl;
iframeContainer.current.appendChild(frame);
setInProgress(false);
});
} }
function path(tail) { return "";
if(props.fling && props.fling.sharing) { }
return `/f/${props.fling.sharing.shareUrl}/${tail}`;
}
return ""; return (
} <>
<div className="container-center">
return( <div className="col-6 col-xl-8 col-lg-10 col-md-12">
<>
<div className="container-center">
<div className="col-6 col-xl-8 col-lg-10 col-md-12"> <h3 className="ml-2"> {props.fling.name} </h3>
<div className="ml-2 text-gray">{infoText}</div>
<h3 className="ml-2"> {props.fling.name} </h3> <div className="card">
<div className="ml-2 text-gray">{infoText}</div> <ul className="tab mx-2">
<li className={`tab-item ${location.pathname !== path("upload") ? "active" : ""}`}>
<Link to={path("files")}>Files</Link>
</li>
{ props.fling.allowUpload
? <li className={`tab-item ${location.pathname === path("upload") ? "active" : ""}`}>
<Link to={path("upload")}>Upload</Link>
</li>
: <></>
}
<div className="card"> <li className="tab-item tab-action">
<ul className="tab mx-2"> <div className="card-title">
<li className={`tab-item ${location.pathname !== path("upload") ? "active": ""}`}> {inProgress
<Link to={path("files")}>Files</Link> ? <button className="m-2 btn btn-xs btn-secondary float-right user-list-loading" disabled
</li> onClick={(ev) => ev.preventDefault()}>
<li className={`tab-item ${location.pathname === path("upload") ? "active": ""}`}> <div className="loading" /> Packaging
<Link to={path("upload")}>Upload</Link>
</li>
<li className="tab-item tab-action">
<div className="card-title">
{inProgress
? <button className="m-2 btn btn-xs btn-secondary float-right user-list-loading" disabled="true"
onClick={(ev) => ev.preventDefault()}>
<div className="loading" /> Packaging
</button> </button>
: <button className="m-2 btn btn-xs btn-secondary float-right" onClick={handleDownload}>Download</button> : <button className="m-2 btn btn-xs btn-secondary float-right" onClick={handleDownload}>Download</button>
} }
</div> </div>
</li> </li>
</ul> </ul>
<div className="card-body">
<Switch>
<Route exact path="/f/:shareId"><Artifacts fling={props.fling} /></Route>
<Route path="/f/:shareId/files"><Artifacts fling={props.fling} /></Route>
<Route path="/f/:shareId/upload"><Upload fling={props.fling} /></Route>
</Switch>
</div>
</div>
<div className="d-hide" ref={iframeContainer} />
<div className="card-body">
<Switch>
<Route exact path="/f/:shareId"><Artifacts fling={props.fling} /></Route>
<Route path="/f/:shareId/files"><Artifacts fling={props.fling} /></Route>
<Route path="/f/:shareId/upload"><Upload fling={props.fling} /></Route>
</Switch>
</div> </div>
</div> </div>
</>
); <div className="d-hide" ref={iframeContainer} />
</div>
</div>
</>
);
} }

Some files were not shown because too many files have changed in this diff Show more