Fling Genesis
Basic Fling backend with admin interface. Features: - Basic admin site structure - Token authorization - Upload artifacts - Change basic fling settings Many things missing still or not fully functional. Signed-off-by: Armin Friedl <dev@friedl.net>
519
.gitignore
vendored
Normal file
|
@ -0,0 +1,519 @@
|
|||
requests/
|
||||
|
||||
|
||||
# Created by https://www.gitignore.io/api/git,vim,web,node,java,react,linux,macos,emacs,maven,gradle,windows,eclipse,intellij+all,visualstudiocode
|
||||
# Edit at https://www.gitignore.io/?templates=git,vim,web,node,java,react,linux,macos,emacs,maven,gradle,windows,eclipse,intellij+all,visualstudiocode
|
||||
|
||||
### Eclipse ###
|
||||
.metadata
|
||||
bin/
|
||||
tmp/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~.nib
|
||||
local.properties
|
||||
.settings/
|
||||
.loadpath
|
||||
.recommenders
|
||||
|
||||
# External tool builders
|
||||
.externalToolBuilders/
|
||||
|
||||
# Locally stored "Eclipse launch configurations"
|
||||
*.launch
|
||||
|
||||
# PyDev specific (Python IDE for Eclipse)
|
||||
*.pydevproject
|
||||
|
||||
# CDT-specific (C/C++ Development Tooling)
|
||||
.cproject
|
||||
|
||||
# CDT- autotools
|
||||
.autotools
|
||||
|
||||
# Java annotation processor (APT)
|
||||
.factorypath
|
||||
|
||||
# PDT-specific (PHP Development Tools)
|
||||
.buildpath
|
||||
|
||||
# sbteclipse plugin
|
||||
.target
|
||||
|
||||
# Tern plugin
|
||||
.tern-project
|
||||
|
||||
# TeXlipse plugin
|
||||
.texlipse
|
||||
|
||||
# STS (Spring Tool Suite)
|
||||
.springBeans
|
||||
|
||||
# Code Recommenders
|
||||
.recommenders/
|
||||
|
||||
# Annotation Processing
|
||||
.apt_generated/
|
||||
|
||||
# Scala IDE specific (Scala & Java development for Eclipse)
|
||||
.cache-main
|
||||
.scala_dependencies
|
||||
.worksheet
|
||||
|
||||
### Eclipse Patch ###
|
||||
# Eclipse Core
|
||||
.project
|
||||
|
||||
# JDT-specific (Eclipse Java Development Tools)
|
||||
.classpath
|
||||
|
||||
# Annotation Processing
|
||||
.apt_generated
|
||||
|
||||
.sts4-cache/
|
||||
|
||||
### Emacs ###
|
||||
# -*- mode: gitignore; -*-
|
||||
*~
|
||||
\#*\#
|
||||
/.emacs.desktop
|
||||
/.emacs.desktop.lock
|
||||
*.elc
|
||||
auto-save-list
|
||||
tramp
|
||||
.\#*
|
||||
|
||||
# Org-mode
|
||||
.org-id-locations
|
||||
*_archive
|
||||
|
||||
# flymake-mode
|
||||
*_flymake.*
|
||||
|
||||
# eshell files
|
||||
/eshell/history
|
||||
/eshell/lastdir
|
||||
|
||||
# elpa packages
|
||||
/elpa/
|
||||
|
||||
# reftex files
|
||||
*.rel
|
||||
|
||||
# AUCTeX auto folder
|
||||
/auto/
|
||||
|
||||
# cask packages
|
||||
.cask/
|
||||
dist/
|
||||
|
||||
# Flycheck
|
||||
flycheck_*.el
|
||||
|
||||
# server auth directory
|
||||
/server/
|
||||
|
||||
# projectiles files
|
||||
.projectile
|
||||
|
||||
# directory configuration
|
||||
.dir-locals.el
|
||||
|
||||
# network security
|
||||
/network-security.data
|
||||
|
||||
|
||||
### Git ###
|
||||
# Created by git for backups. To disable backups in Git:
|
||||
# $ git config --global mergetool.keepBackup false
|
||||
*.orig
|
||||
|
||||
# Created by git when using merge tools for conflicts
|
||||
*.BACKUP.*
|
||||
*.BASE.*
|
||||
*.LOCAL.*
|
||||
*.REMOTE.*
|
||||
*_BACKUP_*.txt
|
||||
*_BASE_*.txt
|
||||
*_LOCAL_*.txt
|
||||
*_REMOTE_*.txt
|
||||
|
||||
### Intellij+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### Intellij+all Patch ###
|
||||
# Ignores the whole .idea folder and all .iml files
|
||||
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
|
||||
|
||||
.idea/
|
||||
|
||||
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
|
||||
|
||||
*.iml
|
||||
modules.xml
|
||||
.idea/misc.xml
|
||||
*.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
.idea/sonarlint
|
||||
|
||||
### Java ###
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
### Linux ###
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### Maven ###
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
.flattened-pom.xml
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# rollup.js default build output
|
||||
|
||||
# Uncomment the public line if your project uses Gatsby
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# https://create-react-app.dev/docs/using-the-public-folder/#docsNav
|
||||
# public
|
||||
|
||||
# Storybook build outputs
|
||||
.out
|
||||
.storybook-out
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# Temporary folders
|
||||
temp/
|
||||
|
||||
### react ###
|
||||
.DS_*
|
||||
**/*.backup.*
|
||||
**/*.back.*
|
||||
|
||||
node_modules
|
||||
|
||||
*.sublime*
|
||||
|
||||
psd
|
||||
thumb
|
||||
sketch
|
||||
|
||||
### Vim ###
|
||||
# Swap
|
||||
[._]*.s[a-v][a-z]
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-rt-v][a-z]
|
||||
[._]ss[a-gi-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
# Session
|
||||
Session.vim
|
||||
Sessionx.vim
|
||||
|
||||
# Temporary
|
||||
.netrwhist
|
||||
|
||||
# Auto-generated tag files
|
||||
tags
|
||||
|
||||
# Persistent undo
|
||||
[._]*.un~
|
||||
|
||||
# Coc configuration directory
|
||||
.vim
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
|
||||
### Web ###
|
||||
*.asp
|
||||
*.cer
|
||||
*.csr
|
||||
*.css
|
||||
*.htm
|
||||
*.html
|
||||
*.js
|
||||
*.jsp
|
||||
*.php
|
||||
*.rss
|
||||
*.wasm
|
||||
*.wat
|
||||
*.xhtml
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
### Gradle ###
|
||||
.gradle
|
||||
build/
|
||||
|
||||
# Ignore Gradle GUI config
|
||||
gradle-app.setting
|
||||
|
||||
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||
!gradle-wrapper.jar
|
||||
|
||||
# Cache of project
|
||||
.gradletasknamecache
|
||||
|
||||
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
|
||||
# gradle/wrapper/gradle-wrapper.properties
|
||||
|
||||
### Gradle Patch ###
|
||||
**/build/
|
||||
|
||||
# End of https://www.gitignore.io/api/git,vim,web,node,java,react,linux,macos,emacs,maven,gradle,windows,eclipse,intellij+all,visualstudiocode
|
117
service/fling/.mvn/wrapper/MavenWrapperDownloader.java
vendored
Normal file
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright 2007-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import java.net.*;
|
||||
import java.io.*;
|
||||
import java.nio.channels.*;
|
||||
import java.util.Properties;
|
||||
|
||||
public class MavenWrapperDownloader {
|
||||
|
||||
private static final String WRAPPER_VERSION = "0.5.6";
|
||||
/**
|
||||
* Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
|
||||
*/
|
||||
private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
|
||||
+ WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
|
||||
|
||||
/**
|
||||
* Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
|
||||
* use instead of the default one.
|
||||
*/
|
||||
private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
|
||||
".mvn/wrapper/maven-wrapper.properties";
|
||||
|
||||
/**
|
||||
* Path where the maven-wrapper.jar will be saved to.
|
||||
*/
|
||||
private static final String MAVEN_WRAPPER_JAR_PATH =
|
||||
".mvn/wrapper/maven-wrapper.jar";
|
||||
|
||||
/**
|
||||
* Name of the property which should be used to override the default download url for the wrapper.
|
||||
*/
|
||||
private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
|
||||
|
||||
public static void main(String args[]) {
|
||||
System.out.println("- Downloader started");
|
||||
File baseDirectory = new File(args[0]);
|
||||
System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
|
||||
|
||||
// If the maven-wrapper.properties exists, read it and check if it contains a custom
|
||||
// wrapperUrl parameter.
|
||||
File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
|
||||
String url = DEFAULT_DOWNLOAD_URL;
|
||||
if(mavenWrapperPropertyFile.exists()) {
|
||||
FileInputStream mavenWrapperPropertyFileInputStream = null;
|
||||
try {
|
||||
mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
|
||||
Properties mavenWrapperProperties = new Properties();
|
||||
mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
|
||||
url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
|
||||
} catch (IOException e) {
|
||||
System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
|
||||
} finally {
|
||||
try {
|
||||
if(mavenWrapperPropertyFileInputStream != null) {
|
||||
mavenWrapperPropertyFileInputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignore ...
|
||||
}
|
||||
}
|
||||
}
|
||||
System.out.println("- Downloading from: " + url);
|
||||
|
||||
File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
|
||||
if(!outputFile.getParentFile().exists()) {
|
||||
if(!outputFile.getParentFile().mkdirs()) {
|
||||
System.out.println(
|
||||
"- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
|
||||
}
|
||||
}
|
||||
System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
|
||||
try {
|
||||
downloadFileFromURL(url, outputFile);
|
||||
System.out.println("Done");
|
||||
System.exit(0);
|
||||
} catch (Throwable e) {
|
||||
System.out.println("- Error downloading");
|
||||
e.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void downloadFileFromURL(String urlString, File destination) throws Exception {
|
||||
if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
|
||||
String username = System.getenv("MVNW_USERNAME");
|
||||
char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
|
||||
Authenticator.setDefault(new Authenticator() {
|
||||
@Override
|
||||
protected PasswordAuthentication getPasswordAuthentication() {
|
||||
return new PasswordAuthentication(username, password);
|
||||
}
|
||||
});
|
||||
}
|
||||
URL website = new URL(urlString);
|
||||
ReadableByteChannel rbc;
|
||||
rbc = Channels.newChannel(website.openStream());
|
||||
FileOutputStream fos = new FileOutputStream(destination);
|
||||
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
|
||||
fos.close();
|
||||
rbc.close();
|
||||
}
|
||||
|
||||
}
|
BIN
service/fling/.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
2
service/fling/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
|
16
service/fling/HELP.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
# Getting Started
|
||||
|
||||
### Reference Documentation
|
||||
For further reference, please consider the following sections:
|
||||
|
||||
* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)
|
||||
* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.2.6.RELEASE/maven-plugin/)
|
||||
* [Spring Boot DevTools](https://docs.spring.io/spring-boot/docs/2.2.6.RELEASE/reference/htmlsingle/#using-boot-devtools)
|
||||
* [Spring Configuration Processor](https://docs.spring.io/spring-boot/docs/2.2.6.RELEASE/reference/htmlsingle/#configuration-metadata-annotation-processor)
|
||||
* [Spring Data JPA](https://docs.spring.io/spring-boot/docs/2.2.6.RELEASE/reference/htmlsingle/#boot-features-jpa-and-spring-data)
|
||||
|
||||
### Guides
|
||||
The following guides illustrate how to use some features concretely:
|
||||
|
||||
* [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/)
|
||||
|
310
service/fling/mvnw
vendored
Executable file
|
@ -0,0 +1,310 @@
|
|||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Maven Start Up Batch script
|
||||
#
|
||||
# Required ENV vars:
|
||||
# ------------------
|
||||
# JAVA_HOME - location of a JDK home dir
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# M2_HOME - location of maven2's installed home dir
|
||||
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
# e.g. to debug Maven itself, use
|
||||
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
if [ -z "$MAVEN_SKIP_RC" ] ; then
|
||||
|
||||
if [ -f /etc/mavenrc ] ; then
|
||||
. /etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.mavenrc" ] ; then
|
||||
. "$HOME/.mavenrc"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# OS specific support. $var _must_ be set to either true or false.
|
||||
cygwin=false;
|
||||
darwin=false;
|
||||
mingw=false
|
||||
case "`uname`" in
|
||||
CYGWIN*) cygwin=true ;;
|
||||
MINGW*) mingw=true;;
|
||||
Darwin*) darwin=true
|
||||
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
|
||||
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
if [ -x "/usr/libexec/java_home" ]; then
|
||||
export JAVA_HOME="`/usr/libexec/java_home`"
|
||||
else
|
||||
export JAVA_HOME="/Library/Java/Home"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
if [ -r /etc/gentoo-release ] ; then
|
||||
JAVA_HOME=`java-config --jre-home`
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$M2_HOME" ] ; then
|
||||
## resolve links - $0 may be a link to maven's home
|
||||
PRG="$0"
|
||||
|
||||
# need this for relative symlinks
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG="`dirname "$PRG"`/$link"
|
||||
fi
|
||||
done
|
||||
|
||||
saveddir=`pwd`
|
||||
|
||||
M2_HOME=`dirname "$PRG"`/..
|
||||
|
||||
# make it fully qualified
|
||||
M2_HOME=`cd "$M2_HOME" && pwd`
|
||||
|
||||
cd "$saveddir"
|
||||
# echo Using m2 at $M2_HOME
|
||||
fi
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched
|
||||
if $cygwin ; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME=`cygpath --unix "$M2_HOME"`
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
|
||||
fi
|
||||
|
||||
# For Mingw, ensure paths are in UNIX format before anything is touched
|
||||
if $mingw ; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME="`(cd "$M2_HOME"; pwd)`"
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
javaExecutable="`which javac`"
|
||||
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
|
||||
# readlink(1) is not available as standard on Solaris 10.
|
||||
readLink=`which readlink`
|
||||
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
|
||||
if $darwin ; then
|
||||
javaHome="`dirname \"$javaExecutable\"`"
|
||||
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
|
||||
else
|
||||
javaExecutable="`readlink -f \"$javaExecutable\"`"
|
||||
fi
|
||||
javaHome="`dirname \"$javaExecutable\"`"
|
||||
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
|
||||
JAVA_HOME="$javaHome"
|
||||
export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$JAVACMD" ] ; then
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
else
|
||||
JAVACMD="`which java`"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
echo "Error: JAVA_HOME is not defined correctly." >&2
|
||||
echo " We cannot execute $JAVACMD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
echo "Warning: JAVA_HOME environment variable is not set."
|
||||
fi
|
||||
|
||||
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
|
||||
|
||||
# traverses directory structure from process work directory to filesystem root
|
||||
# first directory with .mvn subdirectory is considered project base directory
|
||||
find_maven_basedir() {
|
||||
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "Path not specified to find_maven_basedir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
basedir="$1"
|
||||
wdir="$1"
|
||||
while [ "$wdir" != '/' ] ; do
|
||||
if [ -d "$wdir"/.mvn ] ; then
|
||||
basedir=$wdir
|
||||
break
|
||||
fi
|
||||
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
|
||||
if [ -d "${wdir}" ]; then
|
||||
wdir=`cd "$wdir/.."; pwd`
|
||||
fi
|
||||
# end of workaround
|
||||
done
|
||||
echo "${basedir}"
|
||||
}
|
||||
|
||||
# concatenates all lines of a file
|
||||
concat_lines() {
|
||||
if [ -f "$1" ]; then
|
||||
echo "$(tr -s '\n' ' ' < "$1")"
|
||||
fi
|
||||
}
|
||||
|
||||
BASE_DIR=`find_maven_basedir "$(pwd)"`
|
||||
if [ -z "$BASE_DIR" ]; then
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
##########################################################################################
|
||||
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
# This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
##########################################################################################
|
||||
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found .mvn/wrapper/maven-wrapper.jar"
|
||||
fi
|
||||
else
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
|
||||
fi
|
||||
if [ -n "$MVNW_REPOURL" ]; then
|
||||
jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
|
||||
else
|
||||
jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
|
||||
fi
|
||||
while IFS="=" read key value; do
|
||||
case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
|
||||
esac
|
||||
done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Downloading from: $jarUrl"
|
||||
fi
|
||||
wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
|
||||
if $cygwin; then
|
||||
wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
|
||||
fi
|
||||
|
||||
if command -v wget > /dev/null; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found wget ... using wget"
|
||||
fi
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
wget "$jarUrl" -O "$wrapperJarPath"
|
||||
else
|
||||
wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
|
||||
fi
|
||||
elif command -v curl > /dev/null; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found curl ... using curl"
|
||||
fi
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
curl -o "$wrapperJarPath" "$jarUrl" -f
|
||||
else
|
||||
curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
|
||||
fi
|
||||
|
||||
else
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Falling back to using Java to download"
|
||||
fi
|
||||
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
||||
# For Cygwin, switch paths to Windows format before running javac
|
||||
if $cygwin; then
|
||||
javaClass=`cygpath --path --windows "$javaClass"`
|
||||
fi
|
||||
if [ -e "$javaClass" ]; then
|
||||
if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo " - Compiling MavenWrapperDownloader.java ..."
|
||||
fi
|
||||
# Compiling the Java class
|
||||
("$JAVA_HOME/bin/javac" "$javaClass")
|
||||
fi
|
||||
if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
|
||||
# Running the downloader
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo " - Running MavenWrapperDownloader.java ..."
|
||||
fi
|
||||
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
##########################################################################################
|
||||
# End of extension
|
||||
##########################################################################################
|
||||
|
||||
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo $MAVEN_PROJECTBASEDIR
|
||||
fi
|
||||
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME=`cygpath --path --windows "$M2_HOME"`
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
|
||||
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
|
||||
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
|
||||
fi
|
||||
|
||||
# Provide a "standardized" way to retrieve the CLI args that will
|
||||
# work with both Windows and non-Windows executions.
|
||||
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
|
||||
export MAVEN_CMD_LINE_ARGS
|
||||
|
||||
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
exec "$JAVACMD" \
|
||||
$MAVEN_OPTS \
|
||||
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
|
||||
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
|
||||
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
|
182
service/fling/mvnw.cmd
vendored
Normal file
|
@ -0,0 +1,182 @@
|
|||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM https://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Maven Start Up Batch script
|
||||
@REM
|
||||
@REM Required ENV vars:
|
||||
@REM JAVA_HOME - location of a JDK home dir
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM M2_HOME - location of maven2's installed home dir
|
||||
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
|
||||
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
|
||||
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
@REM e.g. to debug Maven itself, use
|
||||
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
|
||||
@echo off
|
||||
@REM set title of command window
|
||||
title %0
|
||||
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
|
||||
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
|
||||
|
||||
@REM set %HOME% to equivalent of $HOME
|
||||
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
|
||||
|
||||
@REM Execute a user defined script before this one
|
||||
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
|
||||
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
|
||||
if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
|
||||
:skipRcPre
|
||||
|
||||
@setlocal
|
||||
|
||||
set ERROR_CODE=0
|
||||
|
||||
@REM To isolate internal variables from possible post scripts, we use another setlocal
|
||||
@setlocal
|
||||
|
||||
@REM ==== START VALIDATION ====
|
||||
if not "%JAVA_HOME%" == "" goto OkJHome
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME not found in your environment. >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
:OkJHome
|
||||
if exist "%JAVA_HOME%\bin\java.exe" goto init
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME is set to an invalid directory. >&2
|
||||
echo JAVA_HOME = "%JAVA_HOME%" >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
@REM ==== END VALIDATION ====
|
||||
|
||||
:init
|
||||
|
||||
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
|
||||
@REM Fallback to current working directory if not found.
|
||||
|
||||
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
|
||||
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
|
||||
|
||||
set EXEC_DIR=%CD%
|
||||
set WDIR=%EXEC_DIR%
|
||||
:findBaseDir
|
||||
IF EXIST "%WDIR%"\.mvn goto baseDirFound
|
||||
cd ..
|
||||
IF "%WDIR%"=="%CD%" goto baseDirNotFound
|
||||
set WDIR=%CD%
|
||||
goto findBaseDir
|
||||
|
||||
:baseDirFound
|
||||
set MAVEN_PROJECTBASEDIR=%WDIR%
|
||||
cd "%EXEC_DIR%"
|
||||
goto endDetectBaseDir
|
||||
|
||||
:baseDirNotFound
|
||||
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
|
||||
cd "%EXEC_DIR%"
|
||||
|
||||
:endDetectBaseDir
|
||||
|
||||
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
|
||||
|
||||
@setlocal EnableExtensions EnableDelayedExpansion
|
||||
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
|
||||
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
|
||||
|
||||
:endReadAdditionalConfig
|
||||
|
||||
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
|
||||
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
|
||||
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
|
||||
|
||||
FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
|
||||
)
|
||||
|
||||
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
if exist %WRAPPER_JAR% (
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Found %WRAPPER_JAR%
|
||||
)
|
||||
) else (
|
||||
if not "%MVNW_REPOURL%" == "" (
|
||||
SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
|
||||
)
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Couldn't find %WRAPPER_JAR%, downloading it ...
|
||||
echo Downloading from: %DOWNLOAD_URL%
|
||||
)
|
||||
|
||||
powershell -Command "&{"^
|
||||
"$webclient = new-object System.Net.WebClient;"^
|
||||
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
|
||||
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
|
||||
"}"^
|
||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
|
||||
"}"
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Finished downloading %WRAPPER_JAR%
|
||||
)
|
||||
)
|
||||
@REM End of extension
|
||||
|
||||
@REM Provide a "standardized" way to retrieve the CLI args that will
|
||||
@REM work with both Windows and non-Windows executions.
|
||||
set MAVEN_CMD_LINE_ARGS=%*
|
||||
|
||||
%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
|
||||
if ERRORLEVEL 1 goto error
|
||||
goto end
|
||||
|
||||
:error
|
||||
set ERROR_CODE=1
|
||||
|
||||
:end
|
||||
@endlocal & set ERROR_CODE=%ERROR_CODE%
|
||||
|
||||
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
|
||||
@REM check for post script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
|
||||
if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
|
||||
:skipRcPost
|
||||
|
||||
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
|
||||
if "%MAVEN_BATCH_PAUSE%" == "on" pause
|
||||
|
||||
if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
|
||||
|
||||
exit /B %ERROR_CODE%
|
173
service/fling/pom.xml
Normal file
|
@ -0,0 +1,173 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
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">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.2.6.RELEASE</version>
|
||||
<relativePath /> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>net.friedl</groupId>
|
||||
<artifactId>fling</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>fling</name>
|
||||
<description>Simple artifact sharing</description>
|
||||
|
||||
<properties>
|
||||
<java.version>11</java.version>
|
||||
<mapstruct.version>1.3.1.Final</mapstruct.version>
|
||||
<bouncycastle.version>1.64</bouncycastle.version>
|
||||
<jwt.version>0.11.1</jwt.version>
|
||||
<spring.version>${project.parent.version}</spring.version>
|
||||
<!-- automatically run annotation processors within the incremental compilation -->
|
||||
<m2e.apt.activation>jdt_apt</m2e.apt.activation>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring boot starter -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Json Web Token -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>${jwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>${jwt.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>${jwt.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Test dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.junit.vintage</groupId>
|
||||
<artifactId>junit-vintage-engine</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Annotation processors -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<!-- Replace @spring.profiles.active@ in application.yml by setting in
|
||||
maven profile See also: profiles section -->
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</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>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<!-- Profile for local development -->
|
||||
<id>local</id>
|
||||
<properties>
|
||||
<spring.profiles.active>local</spring.profiles.active>
|
||||
</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>
|
||||
</profiles>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,13 @@
|
|||
package net.friedl.fling;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class FlingApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(FlingApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package net.friedl.fling;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
@Configuration
|
||||
public class FlingConfiguration {
|
||||
@Bean
|
||||
public ObjectMapper objectMapper() {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.setSerializationInclusion(Include.NON_ABSENT);
|
||||
return objectMapper;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package net.friedl.fling.controller;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
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.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 net.friedl.fling.model.dto.ArtifactDto;
|
||||
import net.friedl.fling.service.ArtifactService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class ArtifactController {
|
||||
|
||||
private ArtifactService artifactService;
|
||||
|
||||
@Autowired
|
||||
public ArtifactController(ArtifactService artifactService) {
|
||||
this.artifactService = artifactService;
|
||||
}
|
||||
|
||||
@GetMapping(path = "/artifacts", params="flingId")
|
||||
public List<ArtifactDto> getArtifacts(@RequestParam Long flingId) {
|
||||
return artifactService.findAllArtifacts(flingId);
|
||||
}
|
||||
|
||||
@GetMapping(path = "/artifacts", params="artifactId")
|
||||
public ResponseEntity<ArtifactDto> getArtifact(@RequestParam Long artifactId) {
|
||||
return ResponseEntity.of(artifactService.findArtifact(artifactId));
|
||||
}
|
||||
|
||||
@PostMapping("/artifacts/{flingId}")
|
||||
public ArtifactDto postArtifact(@PathVariable Long flingId, HttpServletRequest request) throws Exception {
|
||||
return artifactService.storeArtifact(flingId, request.getInputStream());
|
||||
}
|
||||
|
||||
@PatchMapping(path = "/artifacts/{artifactId}", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ArtifactDto patchArtifactDto(@PathVariable Long artifactId, @RequestBody String body) {
|
||||
return artifactService.mergeArtifact(artifactId, body);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package net.friedl.fling.controller;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
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 net.friedl.fling.model.dto.AuthCodeDto;
|
||||
import net.friedl.fling.model.dto.FlingDto;
|
||||
import net.friedl.fling.service.FlingService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class FlingController {
|
||||
|
||||
private FlingService flingService;
|
||||
|
||||
@Autowired
|
||||
public FlingController(FlingService flingService) {
|
||||
this.flingService = flingService;
|
||||
}
|
||||
|
||||
@GetMapping("/fling")
|
||||
public List<FlingDto> getFlings() {
|
||||
return flingService.findAll();
|
||||
}
|
||||
|
||||
@PostMapping("/fling")
|
||||
public void postFling(@RequestBody FlingDto flingDto) {
|
||||
flingService.createFling(flingDto);
|
||||
}
|
||||
|
||||
@PutMapping("/fling/{flingId}")
|
||||
public void putFling(@PathVariable Long flingId, @RequestBody FlingDto flingDto) {
|
||||
flingService.mergeFling(flingId, flingDto);
|
||||
}
|
||||
|
||||
@GetMapping(path = "/fling", params = "flingId")
|
||||
public ResponseEntity<FlingDto> getFling(@RequestParam Long flingId) {
|
||||
return ResponseEntity.of(flingService.findFlingById(flingId));
|
||||
}
|
||||
|
||||
@GetMapping(path = "/fling", params = "shareId")
|
||||
public ResponseEntity<FlingDto> getFlingByShareId(@RequestParam String shareId) {
|
||||
return ResponseEntity.of(flingService.findFlingByShareId(shareId));
|
||||
}
|
||||
|
||||
@GetMapping(path = "/fling/shareExists/{shareId}")
|
||||
public Boolean getShareExists(@PathVariable String shareId) {
|
||||
return flingService.existsShareUrl(shareId);
|
||||
}
|
||||
|
||||
@DeleteMapping("/fling/{flingId}")
|
||||
public void deleteFling(@PathVariable Long flingId) {
|
||||
flingService.deleteFlingById(flingId);
|
||||
}
|
||||
|
||||
@PostMapping("/fling/{flingId}/protect")
|
||||
public void protectFling(@PathVariable Long flingId, @RequestBody AuthCodeDto protectCode) {
|
||||
flingService.protect(flingId, protectCode);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package net.friedl.fling.model.dto;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ArtifactDto {
|
||||
private String name;
|
||||
|
||||
private Long id;
|
||||
|
||||
private String path;
|
||||
|
||||
private String doi;
|
||||
|
||||
private Long size;
|
||||
|
||||
private Integer version;
|
||||
|
||||
private Instant uploadTime;
|
||||
|
||||
private FlingDto fling;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package net.friedl.fling.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AuthCodeDto {
|
||||
String authCode;
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package net.friedl.fling.model.dto;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class FlingDto {
|
||||
private String name;
|
||||
|
||||
private Long id;
|
||||
|
||||
@JsonIgnore
|
||||
private Boolean directDownload;
|
||||
|
||||
@JsonIgnore
|
||||
private Boolean allowUpload;
|
||||
|
||||
@JsonIgnore
|
||||
private Boolean shared;
|
||||
|
||||
@JsonIgnore
|
||||
private String shareUrl;
|
||||
|
||||
@JsonIgnore
|
||||
private Integer expirationClicks;
|
||||
|
||||
@JsonIgnore
|
||||
private Instant expirationTime;
|
||||
|
||||
@JsonProperty("sharing")
|
||||
private void unpackSharing(Map<String, Object> sharing) {
|
||||
this.directDownload = (Boolean) sharing.getOrDefault("directDownload", false);
|
||||
this.allowUpload = (Boolean) sharing.getOrDefault("allowUpload", false);
|
||||
this.shared = (Boolean) sharing.getOrDefault("shared", true);
|
||||
this.shareUrl = (String) sharing.getOrDefault("shareUrl", null);
|
||||
}
|
||||
|
||||
@JsonProperty("sharing")
|
||||
private Map<String, Object> packSharing() {
|
||||
Map<String, Object> sharing = new HashMap<>();
|
||||
sharing.put("directDownload", this.directDownload);
|
||||
sharing.put("allowUpload", this.allowUpload);
|
||||
sharing.put("shared", this.shared);
|
||||
sharing.put("shareUrl", this.shareUrl);
|
||||
|
||||
return sharing;
|
||||
}
|
||||
|
||||
@JsonProperty("expiration")
|
||||
private void unpackExpiration(Map<String, Object> expiration) {
|
||||
String type = (String) expiration.getOrDefault("expirationType", null);
|
||||
if(type == null) return;
|
||||
|
||||
switch(type) {
|
||||
case "time":
|
||||
this.expirationClicks = null;
|
||||
// json can only handle int, long must be given as string
|
||||
this.expirationTime = Instant.ofEpochMilli(Long.parseLong((String) expiration.get("value")));
|
||||
break;
|
||||
case "clicks":
|
||||
this.expirationTime = null;
|
||||
this.expirationClicks = (Integer) expiration.get("value");
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package net.friedl.fling.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class FlingSharingDto {
|
||||
private Boolean allowUpload;
|
||||
|
||||
private Boolean directDownload;
|
||||
|
||||
private String shareUrl;
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package net.friedl.fling.model.mapper;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.mapstruct.Mapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.friedl.fling.model.dto.ArtifactDto;
|
||||
import net.friedl.fling.persistence.entities.ArtifactEntity;
|
||||
|
||||
@Slf4j
|
||||
@Mapper(componentModel = "spring")
|
||||
public abstract class ArtifactMapper {
|
||||
public abstract ArtifactDto map(ArtifactEntity artifactEntity);
|
||||
|
||||
public abstract ArtifactEntity map(ArtifactDto artifactDto);
|
||||
|
||||
public abstract List<ArtifactDto> map(List<ArtifactEntity> artifactEntities);
|
||||
|
||||
public ArtifactDto merge(ArtifactDto originalArtifactDto, Map<String, Object> patch) {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package net.friedl.fling.model.mapper;
|
||||
|
||||
import org.mapstruct.Mapper;
|
||||
|
||||
import net.friedl.fling.model.dto.AuthCodeDto;
|
||||
import net.friedl.fling.persistence.entities.AuthCodeEntity;
|
||||
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface AuthCodeMapper {
|
||||
AuthCodeEntity map(AuthCodeDto authCodeDto);
|
||||
AuthCodeDto map(AuthCodeEntity authCodeEntity);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package net.friedl.fling.model.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.mapstruct.Mapper;
|
||||
|
||||
import net.friedl.fling.model.dto.FlingDto;
|
||||
import net.friedl.fling.persistence.entities.FlingEntity;
|
||||
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface FlingMapper {
|
||||
FlingDto map(FlingEntity flingEntity);
|
||||
|
||||
default Optional<FlingDto> map(Optional<FlingEntity> flingEntity) {
|
||||
return flingEntity.map(f -> map(f));
|
||||
}
|
||||
|
||||
FlingEntity map(FlingDto flingDto);
|
||||
|
||||
List<FlingDto> map(List<FlingEntity> flingEntities);
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
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.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);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package net.friedl.fling.persistence.entities;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.ManyToOne;
|
||||
import javax.persistence.PrePersist;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Entity
|
||||
@Table(name = "Artifact")
|
||||
@Getter @Setter
|
||||
public class ArtifactEntity {
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
private Integer version;
|
||||
|
||||
private String path;
|
||||
|
||||
@Column(unique = true)
|
||||
private String doi;
|
||||
|
||||
private Instant uploadTime;
|
||||
|
||||
private Long size;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
private FlingEntity fling;
|
||||
|
||||
@PrePersist
|
||||
private void prePersist() {
|
||||
this.uploadTime = Instant.now();
|
||||
|
||||
if(this.version == null) this.version = -1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package net.friedl.fling.persistence.entities;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.ManyToOne;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Entity
|
||||
@Table(name = "AuthCode")
|
||||
@Getter @Setter
|
||||
public class AuthCodeEntity {
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String authCode;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
private FlingEntity fling;
|
||||
|
||||
public void setFling(FlingEntity fling) {
|
||||
this.fling = fling;
|
||||
fling.getAuthCodes().add(this);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package net.friedl.fling.persistence.entities;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.persistence.CascadeType;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.OneToMany;
|
||||
import javax.persistence.PostPersist;
|
||||
import javax.persistence.PrePersist;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Entity
|
||||
@Table(name = "Fling")
|
||||
@Getter
|
||||
@Setter
|
||||
public class FlingEntity {
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
private Instant creationTime;
|
||||
|
||||
private Instant expirationTime;
|
||||
|
||||
private Integer expirationClicks;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Boolean directDownload;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Boolean allowUpload;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Boolean shared;
|
||||
|
||||
@Column(unique = true, nullable = false)
|
||||
private String shareUrl;
|
||||
|
||||
@OneToMany(mappedBy = "fling", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private Set<ArtifactEntity> artifacts;
|
||||
|
||||
@OneToMany(mappedBy = "fling", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private Set<AuthCodeEntity> authCodes;
|
||||
|
||||
@PrePersist
|
||||
private void prePersist() {
|
||||
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();
|
||||
}
|
||||
|
||||
@PostPersist
|
||||
private void postPersist() {
|
||||
System.out.println("ID: "+this.id);
|
||||
System.out.println("Share Url: "+this.shareUrl);
|
||||
|
||||
this.shareUrl = this.id+this.shareUrl;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package net.friedl.fling.persistence.repositories;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import net.friedl.fling.persistence.entities.ArtifactEntity;
|
||||
|
||||
public interface ArtifactRepository extends JpaRepository<ArtifactEntity, Long> {
|
||||
Optional<ArtifactEntity> findByDoi(String doi);
|
||||
List<ArtifactEntity> deleteByDoi(String doi);
|
||||
List<ArtifactEntity> findAllByFlingId(Long flingId);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package net.friedl.fling.persistence.repositories;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
import net.friedl.fling.persistence.entities.FlingEntity;
|
||||
|
||||
public interface FlingRepository extends JpaRepository<FlingEntity, Long> {
|
||||
Optional<FlingEntity> findByName(String name);
|
||||
|
||||
Optional<FlingEntity> findByShareUrl(String shareUrl);
|
||||
|
||||
@Query("SELECT COUNT(*) FROM ArtifactEntity a, FlingEntity f where a.fling=f.id and f.id=:flingId")
|
||||
Long countArtifactsById(Long flingId);
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package net.friedl.fling.security;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import net.friedl.fling.service.FlingService;
|
||||
|
||||
@Service
|
||||
public class AuthorizationService {
|
||||
private FlingService flingService;
|
||||
|
||||
@Autowired
|
||||
public AuthorizationService(FlingService flingService) {
|
||||
this.flingService = flingService;
|
||||
}
|
||||
|
||||
public boolean allowUpload(Long flingId) {
|
||||
return flingService
|
||||
.findFlingById(flingId)
|
||||
.orElseThrow()
|
||||
.getAllowUpload();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package net.friedl.fling.security;
|
||||
|
||||
public enum FlingAuthority {
|
||||
FLING_OWNER,
|
||||
FLING_USER
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package net.friedl.fling.security;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Key;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import io.jsonwebtoken.JwtParser;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.Data;
|
||||
|
||||
@Configuration
|
||||
@Data
|
||||
@ConfigurationProperties("fling.security")
|
||||
public class FlingSecurityConfiguration {
|
||||
private List<String> allowedOrigins;
|
||||
|
||||
private String adminUser;
|
||||
|
||||
private String adminPassword;
|
||||
|
||||
private String signingKey;
|
||||
|
||||
private Long jwtExpiration;
|
||||
|
||||
@Bean
|
||||
public Key jwtSigningKey() {
|
||||
byte[] key = signingKey.getBytes(StandardCharsets.UTF_8);
|
||||
return Keys.hmacShaKeyFor(key);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JwtParser jwtParser(Key jwtSignigKey) {
|
||||
return Jwts.parserBuilder()
|
||||
.setSigningKey(jwtSignigKey)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package net.friedl.fling.security;
|
||||
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
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.WebSecurityConfigurerAdapter;
|
||||
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.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.friedl.fling.security.authentication.JwtAuthenticationFilter;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Getter
|
||||
@Setter
|
||||
public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
|
||||
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
private AuthorizationService authorizationService;
|
||||
private FlingSecurityConfiguration securityConfiguration;
|
||||
|
||||
@Autowired
|
||||
public FlingWebSecurityConfigurer(JwtAuthenticationFilter jwtAuthenticationFilter,
|
||||
AuthorizationService authorizationService,
|
||||
FlingSecurityConfiguration securityConfiguraiton) {
|
||||
|
||||
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
||||
this.authorizationService = authorizationService;
|
||||
this.securityConfiguration = securityConfiguraiton;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
//@formatter:off
|
||||
http
|
||||
.csrf().disable()
|
||||
.cors(withDefaults())
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
.authorizeRequests()
|
||||
.antMatchers("/api/auth/**")
|
||||
.permitAll()
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
.antMatchers("/api/**")
|
||||
.hasAuthority(FlingAuthority.FLING_OWNER.name())
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
.antMatchers(HttpMethod.POST, "/api/artifacts/{flingId}/**")
|
||||
.access("hasAuthority('"+FlingAuthority.FLING_USER.name()+"') and @authorizationService.allowUpload(#flingId)")
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
.antMatchers(HttpMethod.GET, "/api/artifacts/**")
|
||||
.hasAuthority(FlingAuthority.FLING_USER.name());
|
||||
//@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
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
// see https://stackoverflow.com/a/43559266
|
||||
|
||||
log.info("Allowed origins: {}", securityConfiguration.getAllowedOrigins());
|
||||
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.setAllowedOrigins(securityConfiguration.getAllowedOrigins());
|
||||
configuration.setAllowedMethods(List.of("*"));
|
||||
|
||||
// setAllowCredentials(true) is important, otherwise:
|
||||
// The value of the 'Access-Control-Allow-Origin' header in the response
|
||||
// must not be the wildcard '*' when the request's credentials mode is
|
||||
// 'include'.
|
||||
configuration.setAllowCredentials(true);
|
||||
|
||||
// setAllowedHeaders is important! Without it, OPTIONS preflight request
|
||||
// will fail with 403 Invalid CORS request
|
||||
configuration.setAllowedHeaders(List.of("Authorization", "Cache-Control", "Content-Type", "Origin"));
|
||||
|
||||
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
|
||||
return source;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
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) {
|
||||
Long flingId = userAuth.getFlingId();
|
||||
String authCode = userAuth.getCode();
|
||||
|
||||
if (!flingService.hasAuthCode(flingId, authCode)) {
|
||||
throw new AccessDeniedException("Wrong fling code");
|
||||
}
|
||||
|
||||
return makeBaseBuilder()
|
||||
.setSubject("user")
|
||||
.claim("fid", flingId)
|
||||
.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;
|
||||
flingId = claims.get("fid", Long.class);
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package net.friedl.fling.security.authentication;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
|
||||
public class FlingToken extends AbstractAuthenticationToken {
|
||||
|
||||
private static final long serialVersionUID = -1112423505610346583L;
|
||||
|
||||
public FlingToken(GrantedFlingAuthority authority) {
|
||||
super(List.of(authority));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
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();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package net.friedl.fling.security.authentication;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
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.Authentication;
|
||||
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;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
private static final String TOKEN_PREFIX = "Bearer ";
|
||||
private static final String HEADER_STRING = "Authorization";
|
||||
|
||||
private AuthenticationService authenticationService;
|
||||
|
||||
@Autowired
|
||||
public JwtAuthenticationFilter(AuthenticationService authenticationService) {
|
||||
this.authenticationService = authenticationService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
String header = request.getHeader(HEADER_STRING);
|
||||
|
||||
if(header == null || !header.startsWith(TOKEN_PREFIX)) {
|
||||
log.warn("Could not find bearer token. No JWT authentication.");
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
String authToken = header.replace(TOKEN_PREFIX, "");
|
||||
|
||||
SecurityContext securityContext = SecurityContextHolder.getContext();
|
||||
|
||||
if(securityContext.getAuthentication() == null) {
|
||||
Authentication authentication = authenticationService.parseAuthentication(authToken);
|
||||
securityContext.setAuthentication(authentication);
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package net.friedl.fling.security.authentication.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class OwnerAuthDto {
|
||||
private String username;
|
||||
private String password;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package net.friedl.fling.security.authentication.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class UserAuthDto {
|
||||
Long flingId;
|
||||
String code;
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package net.friedl.fling.service;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.transaction.Transactional;
|
||||
|
||||
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 net.friedl.fling.model.dto.ArtifactDto;
|
||||
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.repositories.ArtifactRepository;
|
||||
import net.friedl.fling.persistence.repositories.FlingRepository;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class ArtifactService {
|
||||
|
||||
private FlingRepository flingRepository;
|
||||
private ArtifactRepository artifactRepository;
|
||||
private ArtifactMapper artifactMapper;
|
||||
private Archive archive;
|
||||
|
||||
@Autowired
|
||||
public ArtifactService(ArtifactRepository artifactRepository, FlingRepository flingRepository,
|
||||
ArtifactMapper artifactMapper, Archive archive) {
|
||||
this.artifactRepository = artifactRepository;
|
||||
this.flingRepository = flingRepository;
|
||||
this.artifactMapper = artifactMapper;
|
||||
this.archive = archive;
|
||||
}
|
||||
|
||||
public List<ArtifactDto> findAllArtifacts(Long flingId) {
|
||||
return artifactMapper.map(artifactRepository.findAllByFlingId(flingId));
|
||||
}
|
||||
|
||||
public ArtifactDto storeArtifact(Long flingId, InputStream artifact) throws ArchiveException {
|
||||
var flingEntity = flingRepository.findById(flingId).orElseThrow();
|
||||
var archiveId = archive.store(artifact);
|
||||
|
||||
ArtifactEntity artifactEntity = new ArtifactEntity();
|
||||
artifactEntity.setDoi(archiveId);
|
||||
artifactEntity.setFling(flingEntity);
|
||||
|
||||
artifactRepository.save(artifactEntity);
|
||||
|
||||
return artifactMapper.map(artifactEntity);
|
||||
}
|
||||
|
||||
public Optional<ArtifactDto> findArtifact(Long artifactId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public ArtifactDto mergeArtifact(Long artifactId, String body) {
|
||||
JsonParser jsonParser = JsonParserFactory.getJsonParser();
|
||||
Map<String, Object> parsedBody = jsonParser.parseMap(body);
|
||||
|
||||
artifactRepository.findById(artifactId)
|
||||
// map entity to dto
|
||||
.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));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
package net.friedl.fling.service;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
import javax.transaction.Transactional;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.friedl.fling.model.dto.AuthCodeDto;
|
||||
import net.friedl.fling.model.dto.FlingDto;
|
||||
import net.friedl.fling.model.mapper.AuthCodeMapper;
|
||||
import net.friedl.fling.model.mapper.FlingMapper;
|
||||
import net.friedl.fling.persistence.entities.FlingEntity;
|
||||
import net.friedl.fling.persistence.repositories.FlingRepository;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@Transactional
|
||||
public class FlingService {
|
||||
private FlingRepository flingRepository;
|
||||
|
||||
private FlingMapper flingMapper;
|
||||
|
||||
private AuthCodeMapper authCodeMapper;
|
||||
|
||||
@PersistenceContext
|
||||
private EntityManager entityManager;
|
||||
|
||||
@Autowired
|
||||
public FlingService(FlingRepository flingRepository, FlingMapper flingMapper, AuthCodeMapper authCodeMapper) {
|
||||
this.flingRepository = flingRepository;
|
||||
this.flingMapper = flingMapper;
|
||||
this.authCodeMapper = authCodeMapper;
|
||||
}
|
||||
|
||||
public List<FlingDto> findAll() {
|
||||
return flingMapper.map(flingRepository.findAll());
|
||||
}
|
||||
|
||||
public void createFling(FlingDto flingDto) {
|
||||
if (!StringUtils.hasText(flingDto.getShareUrl())) {
|
||||
flingDto.setShareUrl(generateShareUrl());
|
||||
}
|
||||
|
||||
var flingEntity = flingMapper.map(flingDto);
|
||||
flingRepository.save(flingEntity);
|
||||
}
|
||||
|
||||
public Boolean existsShareUrl(String shareUrl) {
|
||||
return !flingRepository.findByShareUrl(shareUrl).isEmpty();
|
||||
}
|
||||
|
||||
public void mergeFling(Long flingId, FlingDto flingDto) {
|
||||
var flingEntity = flingRepository.getOne(flingId);
|
||||
|
||||
mergeNonEmpty(flingDto::getAllowUpload, flingEntity::setAllowUpload);
|
||||
mergeNonEmpty(flingDto::getDirectDownload, flingEntity::setDirectDownload);
|
||||
mergeNonEmpty(flingDto::getExpirationClicks, flingEntity::setExpirationClicks);
|
||||
mergeNonEmpty(flingDto::getExpirationTime, flingEntity::setExpirationTime);
|
||||
mergeNonEmpty(flingDto::getName, flingEntity::setName);
|
||||
mergeNonEmpty(flingDto::getShared, flingEntity::setShared);
|
||||
mergeNonEmpty(flingDto::getShareUrl, flingEntity::setShareUrl);
|
||||
}
|
||||
|
||||
public Optional<FlingDto> findFlingById(Long flingId) {
|
||||
return flingMapper.map(flingRepository.findById(flingId));
|
||||
}
|
||||
|
||||
public Optional<FlingDto> findFlingByShareId(String shareUrl) {
|
||||
return flingMapper.map(flingRepository.findByShareUrl(shareUrl));
|
||||
}
|
||||
|
||||
public void deleteFlingById(Long flingId) {
|
||||
flingRepository.deleteById(flingId);
|
||||
}
|
||||
|
||||
public boolean hasAuthCode(Long flingId, String authCode) {
|
||||
return flingRepository.getOne(flingId).getAuthCodes()
|
||||
.stream().anyMatch(ae -> ae.getAuthCode().equals(authCode));
|
||||
}
|
||||
|
||||
public void protect(Long flingId, AuthCodeDto authCodeDto) {
|
||||
var fling = flingRepository.getOne(flingId);
|
||||
var authCode = authCodeMapper.map(authCodeDto);
|
||||
|
||||
authCode.setFling(fling);
|
||||
}
|
||||
|
||||
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 static String generateShareUrl() {
|
||||
var key = KeyGenerators
|
||||
.secureRandom(16)
|
||||
.generateKey();
|
||||
|
||||
return Base64.getUrlEncoder().encodeToString(key)
|
||||
// replace all special chars [=-_] in RFC 4648
|
||||
// "URL and Filename safe" table with characters from
|
||||
// [A-Za-z0-9]. Hence, the generated share url will only consist
|
||||
// of [A-Za-z0-9].
|
||||
.replace('=', 'q')
|
||||
.replace('_', 'u')
|
||||
.replace('-', 'd');
|
||||
}
|
||||
|
||||
private <T> void mergeNonEmpty(Supplier<T> sup, Consumer<T> con) {
|
||||
T r = sup.get();
|
||||
if(r != null) con.accept(r);
|
||||
}
|
||||
}
|
31
service/fling/src/main/resources/application-local.yml
Normal file
|
@ -0,0 +1,31 @@
|
|||
spring:
|
||||
datasource:
|
||||
url: jdbc:h2:file:~/Desktop/testdb;AUTO_SERVER=TRUE;DB_CLOSE_ON_EXIT=FALSE;IFEXISTS=TRUE;
|
||||
driverClassName: org.h2.Driver
|
||||
username: sa
|
||||
password:
|
||||
jpa:
|
||||
hibernate.ddl-auto: update
|
||||
database-platform: org.hibernate.dialect.H2Dialect
|
||||
servlet:
|
||||
multipart.max-file-size: -1
|
||||
multipart.max-request-size: -1
|
||||
|
||||
logging.level:
|
||||
root: WARN
|
||||
net.friedl: TRACE
|
||||
# org.springframework.security: TRACE
|
||||
# org.springframework: WARN
|
||||
# org.hibernate: WARN
|
||||
# spring.http.log-request-details: true
|
||||
|
||||
fling:
|
||||
archive.fileystem.directory: "/home/armin/Desktop/fling"
|
||||
security:
|
||||
allowed-origins:
|
||||
- "https://friedl.net"
|
||||
- "http://localhost:3000"
|
||||
admin-user: "${FLING_ADMIN_USER:admin}"
|
||||
admin-password: "${FLING_ADMIN_PASSWORD:123}"
|
||||
signing-key: "${FLING_SIGNING_KEY:changeitchangeitchangeitchangeit}"
|
||||
jwt-expiration: "${FLING_JWT_EXPIRATION:180000}"
|
1
service/fling/src/main/resources/application.yml
Normal file
|
@ -0,0 +1 @@
|
|||
spring.profiles.active: "@spring.profiles.active@" # To be replaced by maven according to profile settings
|
287
service/fling/src/main/resources/fling.yaml
Normal file
|
@ -0,0 +1,287 @@
|
|||
---
|
||||
openapi: 3.0.2
|
||||
info:
|
||||
title: Fling
|
||||
version: 1.0.0
|
||||
description: A project based API for publishing and sharing digital artifacts
|
||||
contact:
|
||||
name: Armin Friedl
|
||||
email: dev@friedl.net
|
||||
paths:
|
||||
/fling:
|
||||
summary: A fling share
|
||||
description: |-
|
||||
A grouping of shared objects. Settings for sharing and expiration are
|
||||
associated with a fling.
|
||||
get:
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Fling'
|
||||
description: List of metadata for all flings
|
||||
summary: Get a list of metadata for all flings
|
||||
post:
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Fling'
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
description: Fling created successfuly
|
||||
summary: Create a new fling
|
||||
/fling/{flingId}:
|
||||
get:
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Fling'
|
||||
description: A fling object containing all metadata of the fling
|
||||
summary: Get metadata for a fling
|
||||
description: Expiration, sharing and general information of a fling.
|
||||
delete:
|
||||
responses:
|
||||
"200":
|
||||
description: Fling deleted successfully
|
||||
patch:
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Fling'
|
||||
required: true
|
||||
parameters:
|
||||
- name: id
|
||||
description: The unique fling id
|
||||
schema:
|
||||
format: int64
|
||||
type: integer
|
||||
in: query
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
description: Fling was successfully updated
|
||||
parameters:
|
||||
- name: flingId
|
||||
description: Unique id of the fling
|
||||
schema:
|
||||
format: int64
|
||||
type: integer
|
||||
in: path
|
||||
required: true
|
||||
/f:
|
||||
summary: Endpoint for accessing flings by external users
|
||||
/f/{shareId}:
|
||||
get:
|
||||
responses:
|
||||
"302":
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
type: string
|
||||
description: |-
|
||||
If the fling is marked as direct download, the user will be directly redirected
|
||||
to /f/{sharId}/download which starts the fling download.
|
||||
"200":
|
||||
content:
|
||||
application/json: {}
|
||||
description: sdf
|
||||
parameters:
|
||||
- name: shareId
|
||||
description: |-
|
||||
A share id with which a fling can be uniquely identified. The share id might be
|
||||
the fling id also used in /fling endpoints, another artificially generated id or
|
||||
a customURL given by the fling creator
|
||||
schema:
|
||||
type: string
|
||||
in: path
|
||||
required: true
|
||||
/fling/{flingId}/artifact:
|
||||
summary: Upload an object to the fling identified by id
|
||||
get:
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Artifact'
|
||||
description: A list of metadata of all objects within this fling
|
||||
summary: Retrieve a list of metadata all objects within this fling
|
||||
post:
|
||||
requestBody:
|
||||
description: "Content type is any media type from \nhttps://www.iana.org/assignments/media-types/media-types.xhtml.\n\
|
||||
The content can by arbitrary payload and will be stored as-is."
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
format: binary
|
||||
type: string
|
||||
required: true
|
||||
parameters:
|
||||
- name: name
|
||||
description: The name of the object
|
||||
schema:
|
||||
type: string
|
||||
in: query
|
||||
required: true
|
||||
- name: path
|
||||
description: |-
|
||||
A path under which the object should be stored. Nested paths must be delimited
|
||||
by forward slash '/'. The path might or might not start with a '/', it will always
|
||||
be interpreted as absolute starting from the fling root.
|
||||
schema:
|
||||
type: string
|
||||
in: query
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Artifact'
|
||||
description: Return the metadata of a fling object after successful request
|
||||
parameters:
|
||||
- name: flingId
|
||||
description: Unique id for the fling
|
||||
schema:
|
||||
format: int64
|
||||
type: integer
|
||||
in: path
|
||||
required: true
|
||||
/fling/{flingId}/artifact/{objectId}:
|
||||
summary: Endpoint for interacting with individual objects within a fling
|
||||
get:
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Artifact'
|
||||
description: Return the metadata of a fling object after successful request
|
||||
summary: Retrive the metadata of an object within a fling
|
||||
put:
|
||||
requestBody:
|
||||
description: "Content type is any media type from \nhttps://www.iana.org/assignments/media-types/media-types.xhtml.\n\
|
||||
The content can by arbitrary payload and will be stored as-is."
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
format: binary
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Artifact'
|
||||
description: Return the metadata of a fling object after successful request
|
||||
summary: Create a new version of the object
|
||||
parameters:
|
||||
- name: flingId
|
||||
description: The unique id of the fling
|
||||
schema:
|
||||
format: int64
|
||||
type: integer
|
||||
in: path
|
||||
required: true
|
||||
- name: objectId
|
||||
description: |-
|
||||
The unique id of the object within its fling. The id is not necessarily globally
|
||||
unique across all flings.
|
||||
schema:
|
||||
format: int64
|
||||
type: integer
|
||||
in: path
|
||||
required: true
|
||||
components:
|
||||
schemas:
|
||||
Fling:
|
||||
description: |-
|
||||
Fling containing all metadata of a fling, including general information,
|
||||
sharing settings and expiration settings.
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
description: Human readable name of the fling
|
||||
type: string
|
||||
id:
|
||||
format: int64
|
||||
description: Unique id of the fling
|
||||
type: integer
|
||||
expiration:
|
||||
description: Expiration settings
|
||||
type: object
|
||||
properties:
|
||||
expirationType:
|
||||
description: Expiration type
|
||||
enum:
|
||||
- Time
|
||||
- Clicks
|
||||
type: string
|
||||
value:
|
||||
description: Parsable string representation for the expiration type
|
||||
type: string
|
||||
sharing:
|
||||
description: Settings for sharing
|
||||
type: object
|
||||
properties:
|
||||
directDownload:
|
||||
description: Accessing the share will immediately prompt a download
|
||||
type: boolean
|
||||
upload:
|
||||
description: Allow external users to upload to the share
|
||||
type: boolean
|
||||
customURL:
|
||||
description: |-
|
||||
Makes the fling available under a custom URL. The URL is only
|
||||
allowed to have one path element. The allowed characters are
|
||||
[A-Za-z0-9-._~], that is, the unreserved URL characters of
|
||||
https://www.ietf.org/rfc/rfc3986 excluding the forward
|
||||
slash. The customURL must be unique across all flings
|
||||
of a Fling application deployment.
|
||||
type: string
|
||||
Artifact:
|
||||
description: An object in a fling share
|
||||
required:
|
||||
- name
|
||||
- doi
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
description: The name of the object
|
||||
type: string
|
||||
id:
|
||||
format: int64
|
||||
description: The unique id of the object within its fling
|
||||
type: integer
|
||||
path:
|
||||
description: |-
|
||||
The path of the object within its fling. Nested paths are separated by a forward
|
||||
slash '/'. A path may or may not start with '/'. A path will always be interpreted
|
||||
as absolute with the fling of the object as root.
|
||||
type: string
|
||||
doi:
|
||||
description: "A unique and stable id for the fling object. This id is unique\
|
||||
\ across all flings\nand versions of an object within a Fling deployment.\
|
||||
\ A doi is never assigned twice\nby a Fling deployment, even if the object\
|
||||
\ it referred to (or a version thereof) \nwas delted. A doi might not\
|
||||
\ resolve to an object if the object was deleted."
|
||||
type: string
|
||||
version:
|
||||
description: The version of the object
|
||||
type: integer
|
||||
fling:
|
||||
format: int64
|
||||
description: The id of the fling this object belongs to
|
||||
type: integer
|
|
@ -0,0 +1,13 @@
|
|||
package net.friedl.fling;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class FlingApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package net.friedl.fling.controller;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
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.test.web.servlet.MockMvc;
|
||||
|
||||
import net.friedl.fling.model.dto.ArtifactDto;
|
||||
import net.friedl.fling.service.ArtifactService;
|
||||
|
||||
@WebMvcTest(ArtifactController.class)
|
||||
class ArtifactControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@MockBean
|
||||
private ArtifactService artifactService;
|
||||
|
||||
@Test
|
||||
void testGetArtifacts_noArtifacts_empty() throws Exception {
|
||||
var flingId = 123L;
|
||||
|
||||
when(artifactService.findAllArtifacts(flingId)).thenReturn(List.of());
|
||||
|
||||
mvc.perform(get("/api/fling/{flingId}/artifact", flingId)).andExpect(jsonPath("$", hasSize(0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetArtifacts_hasArtifacts_allArtifacts() throws Exception {
|
||||
var flingId = 123L;
|
||||
var artifactName = "TEST";
|
||||
|
||||
ArtifactDto artifactDto = new ArtifactDto();
|
||||
artifactDto.setName(artifactName);
|
||||
|
||||
when(artifactService.findAllArtifacts(flingId)).thenReturn(List.of(artifactDto));
|
||||
|
||||
mvc.perform(get("/api/fling/{flingId}/artifact", flingId)).andExpect(jsonPath("$", hasSize(1)))
|
||||
.andExpect(jsonPath("$[0].name", equalTo(artifactName)));
|
||||
}
|
||||
}
|
2
web/fling/.env.development.local
Normal file
|
@ -0,0 +1,2 @@
|
|||
REACT_APP_API=http://localhost:8080/api
|
||||
REACT_APP_LOGLEVEL=trace
|
68
web/fling/README.md
Normal file
|
@ -0,0 +1,68 @@
|
|||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.<br />
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.<br />
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.<br />
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.<br />
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br />
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
|
16282
web/fling/package-lock.json
generated
Normal file
47
web/fling/package.json
Normal file
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"name": "fling",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.5.0",
|
||||
"@testing-library/user-event": "^7.2.1",
|
||||
"axios": "^0.19.2",
|
||||
"classnames": "^2.2.6",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"loglevel": "^1.6.8",
|
||||
"node-sass": "^4.14.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-redux": "^7.2.0",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "3.4.1",
|
||||
"redux": "^4.0.5",
|
||||
"spectre.css": "^0.5.8"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"redux-devtools": "^3.5.0"
|
||||
}
|
||||
}
|
BIN
web/fling/public/favicon.ico
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
web/fling/public/fling.ico
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
web/fling/public/fling192.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
web/fling/public/fling512.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
web/fling/public/logo192.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
web/fling/public/logo512.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
25
web/fling/public/manifest.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"short_name": "Fling",
|
||||
"name": "Fling",
|
||||
"icons": [
|
||||
{
|
||||
"src": "fling.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "fling192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "fling512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
3
web/fling/public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
34
web/fling/src/App.jsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import log from 'loglevel';
|
||||
import React from 'react';
|
||||
|
||||
import {Switch, Route, Redirect} from "react-router-dom";
|
||||
|
||||
import request, {isOwner} from './util/request';
|
||||
|
||||
import Login from './components/Login';
|
||||
import Fling from './components/Fling';
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path="/login" component={Login} />
|
||||
<OwnerRoute exact path="(/|/flings)"><Fling /></OwnerRoute>
|
||||
<Route match="*">Not implemented</Route>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
// A wrapper for <Route> that redirects to the login
|
||||
// screen if you're not yet authenticated.
|
||||
function OwnerRoute({ children, ...rest }) {
|
||||
return (
|
||||
<Route
|
||||
{...rest}
|
||||
render={({ location }) => {
|
||||
log.info(request.defaults);
|
||||
if(isOwner()) { return children; }
|
||||
else { return <Redirect to={{pathname: "/login", state: {from: location}}} />; }
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
28
web/fling/src/components/Error.jsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import React, {useState} from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import './Error.scss';
|
||||
|
||||
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() : "" }
|
||||
</>
|
||||
);
|
||||
}
|
0
web/fling/src/components/Error.scss
Normal file
28
web/fling/src/components/Fling.jsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import log from 'loglevel';
|
||||
import React, {useState} from 'react';
|
||||
|
||||
import Navbar from './Navbar';
|
||||
import FlingList from './FlingList';
|
||||
import FlingContent from './FlingContent';
|
||||
|
||||
import {HashRouter} from 'react-router-dom';
|
||||
|
||||
import './Fling.scss';
|
||||
|
||||
export default function Fling() {
|
||||
const [activeFling, setActiveFling] = useState(undefined);
|
||||
|
||||
return(
|
||||
<div>
|
||||
<Navbar />
|
||||
<div className="container">
|
||||
<div className="columns mt-2">
|
||||
<div className="column col-sm-12 col-lg-3 col-2"> <FlingList setActiveFlingFn={setActiveFling} activeFling={activeFling} /> </div>
|
||||
<div className="column col-sm-12">
|
||||
<FlingContent activeFling={activeFling} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
0
web/fling/src/components/Fling.scss
Normal file
72
web/fling/src/components/FlingArtifacts.jsx
Normal file
|
@ -0,0 +1,72 @@
|
|||
import log from 'loglevel';
|
||||
import React, {useState, useEffect, useRef} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {artifactClient} from '../util/flingclient';
|
||||
|
||||
import './FlingArtifacts.scss';
|
||||
|
||||
function FlingArtifactControl(props) {
|
||||
return(
|
||||
<div className={`btn-group ${props.hidden ? "d-invisible": "d-visible"}`}>
|
||||
<button className="btn btn-sm"><i className="icon icon-delete"/></button>
|
||||
<button className="btn btn-sm"><i className="icon icon-edit"/></button>
|
||||
<button className="btn btn-sm"><i className="icon icon-download"/></button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FlingArtifactRow(props) {
|
||||
let [hovered, setHovered] = useState(false);
|
||||
|
||||
return(
|
||||
<tr key={props.artifact.id} className="artifact-row" onMouseOver={() => setHovered(true)} onMouseOut={() => setHovered(false)}>
|
||||
<td>{props.artifact.name}</td>
|
||||
<td>{props.artifact.version}</td>
|
||||
<td/>
|
||||
<td><FlingArtifactControl hidden={!hovered} /></td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FlingArtifacts(props) {
|
||||
const [artifacts, setArtifacts] = useState([]);
|
||||
useEffect(getArtifacts, [props.activeFling]);
|
||||
|
||||
return (
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Date</th>
|
||||
<th>Size</th>
|
||||
<th/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{artifacts}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
function getArtifacts() {
|
||||
if (!props.activeFling) {
|
||||
log.debug("No fling active. Not getting artifacts.");
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(`Fling ${props.activeFling} active. Getting 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} />);
|
||||
}
|
||||
|
||||
setArtifacts(artifacts);
|
||||
});
|
||||
}
|
||||
}
|
34
web/fling/src/components/FlingArtifacts.scss
Normal file
|
@ -0,0 +1,34 @@
|
|||
@import "../styles/base.scss";
|
||||
@import "~spectre.css/src/_variables.scss";
|
||||
|
||||
.table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
thead th {
|
||||
&:nth-child(1) {
|
||||
width:60%;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
width:20%;
|
||||
}
|
||||
&:nth-child(3) {
|
||||
width:20%;
|
||||
}
|
||||
// control box
|
||||
&:nth-child(4) {
|
||||
text-align: right;
|
||||
width: 4*$control-size;
|
||||
}
|
||||
}
|
||||
|
||||
tbody td {
|
||||
&:nth-child(4) {
|
||||
text-align: right;
|
||||
width: 4*$control-size;
|
||||
}
|
||||
}
|
||||
|
||||
.artifact-row:hover {
|
||||
background-color: $secondary-color;
|
||||
}
|
43
web/fling/src/components/FlingContent.jsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import log from 'loglevel';
|
||||
import React, {useState, useEffect, useRef} from 'react';
|
||||
import {Switch, Route, Redirect, HashRouter, useLocation} from "react-router-dom";
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {artifactClient} from '../util/flingclient';
|
||||
|
||||
import FlingArtifacts from './FlingArtifacts';
|
||||
import Upload from './Upload';
|
||||
import Settings from './Settings';
|
||||
|
||||
import './FlingContent.scss';
|
||||
|
||||
export default function FlingContent(props) {
|
||||
let location = useLocation();
|
||||
|
||||
return(
|
||||
<div className="fling-content p-2">
|
||||
<ul className="tab tab-block mt-0">
|
||||
<li className={`tab-item ${location.hash !== "#/upload" && location.hash !== "#/settings" ? "active": ""}`}>
|
||||
<a href="#/files">Files</a>
|
||||
</li>
|
||||
<li className={`tab-item ${location.hash === "#/upload"? "active": ""}`}>
|
||||
<a href="#/upload">Upload</a>
|
||||
</li>
|
||||
<li className={`tab-item ${location.hash === "#/settings"? "active": ""}`}>
|
||||
<a href="#/settings">Settings</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="mt-2">
|
||||
<HashRouter>
|
||||
<Switch>
|
||||
<Route exact path={["/files","/"]}><FlingArtifacts activeFling={props.activeFling} /></Route>
|
||||
<Route path="/upload"><Upload activeFling={props.activeFling} /></Route>
|
||||
<Route path="/settings"><div><Settings activeFling={props.activeFling}/></div></Route>
|
||||
</Switch>
|
||||
</HashRouter>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
6
web/fling/src/components/FlingContent.scss
Normal file
|
@ -0,0 +1,6 @@
|
|||
@import "../styles/base.scss";
|
||||
|
||||
.fling-content {
|
||||
@extend %shadow;
|
||||
background-color: #ffffff;
|
||||
}
|
39
web/fling/src/components/FlingList.jsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import log from 'loglevel';
|
||||
import React, {useState, useEffect} from 'react';
|
||||
|
||||
import {flingClient} from '../util/flingclient';
|
||||
|
||||
import FlingTile from './FlingTile';
|
||||
|
||||
import './FlingList.scss';
|
||||
|
||||
export default function FlingList(props) {
|
||||
const [flings, setFlings] = useState([]);
|
||||
useEffect(() => { refreshFlingList(); }, [props.activeFling]);
|
||||
|
||||
return(
|
||||
<div className="panel">
|
||||
<div className="panel-header p-2">
|
||||
<h5>My Flings</h5>
|
||||
</div>
|
||||
<div className="panel-body p-0">
|
||||
{flings}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
async function refreshFlingList() {
|
||||
let flings = await flingClient.getFlings();
|
||||
let newFlings = [];
|
||||
|
||||
for (let fling of flings) {
|
||||
let flingTile = <FlingTile fling={fling}
|
||||
key={fling.id}
|
||||
activeFling={props.activeFling}
|
||||
setActiveFlingFn={props.setActiveFlingFn}
|
||||
refreshFlingListFn={refreshFlingList} />;
|
||||
newFlings.push(flingTile);
|
||||
}
|
||||
setFlings(newFlings);
|
||||
}
|
||||
}
|
12
web/fling/src/components/FlingList.scss
Normal file
|
@ -0,0 +1,12 @@
|
|||
@import "../styles/base.scss";
|
||||
@import "~spectre.css/src/_variables.scss";
|
||||
|
||||
.panel {
|
||||
background-color: $light-color;
|
||||
border: none;
|
||||
@extend %shadow;
|
||||
}
|
||||
|
||||
.panel .panel-body {
|
||||
overflow-y: visible;
|
||||
}
|
88
web/fling/src/components/FlingTile.jsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
import log from 'loglevel';
|
||||
import React, {useRef, useState} from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {flingClient} from '../util/flingclient';
|
||||
|
||||
import './FlingTile.scss';
|
||||
|
||||
function TileAction(props) {
|
||||
let shareUrlRef = useRef(null);
|
||||
|
||||
return(
|
||||
<div className="tile-action dropdown">
|
||||
<a className="btn btn-link btn dropdown-toggle" tabIndex="0">
|
||||
<i className="icon icon-more-vert" />
|
||||
</a>
|
||||
<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.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">
|
||||
<a href="#" className="text-warning" onClick={deleteFling}>
|
||||
<i className="icon icon-delete mr-1" /> Remove
|
||||
</a>
|
||||
</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) {
|
||||
let tileClasses = classNames(
|
||||
"tile", "tile-centered", "p-2", "c-hand",
|
||||
{"active": props.activeFling === props.fling.id}
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={tileClasses}>
|
||||
<div className="tile-content" onClick={() => activateTile(props.fling.id)}>
|
||||
<div className="tile-title">{props.fling.name}</div>
|
||||
<small className="tile-subtitle text-gray">14MB · Public · 1 Jan, 2017</small>
|
||||
</div>
|
||||
<TileAction fling={props.fling} refreshFlingListFn={props.refreshFlingListFn} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function activateTile() {
|
||||
log.debug(`Activating fling ${props.fling.id}`);
|
||||
props.setActiveFlingFn(props.fling.id);
|
||||
}
|
||||
}
|
24
web/fling/src/components/FlingTile.scss
Normal file
|
@ -0,0 +1,24 @@
|
|||
@import "../styles/base.scss";
|
||||
@import "~spectre.css/src/_variables.scss";
|
||||
|
||||
.tile.active {
|
||||
background-color: $gray-color-light;
|
||||
}
|
||||
|
||||
.tile:hover {
|
||||
background-color: $secondary-color;
|
||||
}
|
||||
|
||||
.divider {
|
||||
transform: scale(0.8, 1) translate(-10%,0);
|
||||
}
|
||||
|
||||
.input-group-addon.input-group-addon-sm {
|
||||
padding-top: 0.05rem;
|
||||
padding-bottom: 0.05rem;
|
||||
}
|
||||
|
||||
.form-input.input-share-id {
|
||||
cursor: text;
|
||||
box-shadow: none;
|
||||
}
|
87
web/fling/src/components/Login.jsx
Normal file
|
@ -0,0 +1,87 @@
|
|||
import log from 'loglevel';
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import {useHistory, useLocation} from 'react-router-dom';
|
||||
|
||||
import request, {setAuth} from '../util/request';
|
||||
|
||||
import './Login.scss';
|
||||
|
||||
import Error from './Error';
|
||||
|
||||
export default () => {
|
||||
const [errors, setErrors] = useState([]);
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const { from } = location.state || { from: { pathname: "/" } };
|
||||
|
||||
useEffect(() => setAuth(null), []);
|
||||
|
||||
return (
|
||||
<div className="container-center">
|
||||
<div>
|
||||
<Error errors={errors} clearErrors={clearErrors} >
|
||||
<form className="container-login" 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="form-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="bottom-text">Ready. Set. Fling.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
function handleSubmit(ev) {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
function clearErrors() {
|
||||
setErrors([]);
|
||||
}
|
||||
}
|
19
web/fling/src/components/Login.scss
Normal file
|
@ -0,0 +1,19 @@
|
|||
@import "../styles/base.scss";
|
||||
|
||||
.container-login {
|
||||
@extend %shadow;
|
||||
padding: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.bottom-text {
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
color: $light-grey;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.form-action-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
30
web/fling/src/components/Navbar.jsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import log from 'loglevel';
|
||||
import React from 'react';
|
||||
|
||||
import request from '../util/request';
|
||||
|
||||
import './Navbar.scss';
|
||||
import send from './send.svg';
|
||||
|
||||
export default function Navbar() {
|
||||
return (
|
||||
<header className="navbar">
|
||||
<section className="navbar-section">
|
||||
<a href="/" className="navbar-brand">
|
||||
<img src={send} alt="Fling logo"/>
|
||||
Fling
|
||||
</a>
|
||||
</section>
|
||||
<section className="navbar-center">
|
||||
<div className="input-group input-inline">
|
||||
<input className="form-input input-sm" type="text" placeholder="Search" />
|
||||
<button className="btn btn-sm btn-link input-group-btn"><i className="icon icon-search"/></button>
|
||||
</div>
|
||||
</section>
|
||||
<section className="navbar-section navbar-control">
|
||||
<button className="btn btn-sm btn-link"><i className="icon icon-plus"/> New</button>
|
||||
<a className="btn btn-sm btn-link" href="/login"><i className="icon icon-shutdown"/> Logout</a>
|
||||
</section>
|
||||
</header>
|
||||
);
|
||||
}
|
46
web/fling/src/components/Navbar.scss
Normal file
|
@ -0,0 +1,46 @@
|
|||
$primary-color: rgb(235, 236, 237);
|
||||
|
||||
@import "../styles/base.scss";
|
||||
@import "~spectre.css/src/_mixins.scss";
|
||||
@import "~spectre.css/src/_variables.scss";
|
||||
@import "~spectre.css/src/_buttons.scss";
|
||||
|
||||
.navbar {
|
||||
@extend %shadow;
|
||||
padding: 0.2rem;
|
||||
background-color: #323334;
|
||||
color: #ebeced;
|
||||
}
|
||||
|
||||
.navbar .navbar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.1rem;
|
||||
color: $primary-color;
|
||||
}
|
||||
|
||||
.navbar input {
|
||||
background-color: #464748;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.navbar input + button.input-group-btn {
|
||||
background-color: #464748;
|
||||
border: none;
|
||||
border-left: solid;
|
||||
border-left-color: #323334;
|
||||
border-left-width: thin;
|
||||
}
|
||||
|
||||
.navbar-brand img {
|
||||
height: 1.1rem;
|
||||
margin-right: 0.3rem;
|
||||
margin-left: 0.2rem;
|
||||
}
|
||||
|
||||
.navbar-control {
|
||||
.btn, a {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
129
web/fling/src/components/Settings.jsx
Normal file
|
@ -0,0 +1,129 @@
|
|||
import log from 'loglevel';
|
||||
import React, {useState, useEffect, useRef} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {flingClient} from '../util/flingclient';
|
||||
|
||||
import './Settings.scss';
|
||||
|
||||
export default function Settings(props) {
|
||||
let [fling, setFling] = useState({name: "", sharing: {directDownload: false, allowUpload: true, shared: true, shareUrl: ""}});
|
||||
let [shareUrlUnique, setShareUrlUnique] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if(props.activeFling) {
|
||||
flingClient.getFling(props.activeFling)
|
||||
.then(result => {
|
||||
setFling(result);
|
||||
});
|
||||
}
|
||||
}, [props.activeFling]);
|
||||
|
||||
function toggleSharing(ev) {
|
||||
let f = {...fling};
|
||||
let s = {...fling.sharing};
|
||||
|
||||
if(ev.currentTarget.id === "direct-download") {
|
||||
if(ev.currentTarget.checked) {
|
||||
s.directDownload = true;
|
||||
s.shared = true;
|
||||
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 handleSubmit(ev) {
|
||||
ev.preventDefault();
|
||||
log.info(fling);
|
||||
flingClient.putFling(props.activeFling, fling);
|
||||
}
|
||||
|
||||
return(
|
||||
<div className="container">
|
||||
{log.info(props)}
|
||||
<div className="columns">
|
||||
<div className="column col-6">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="input-name">Name</label>
|
||||
<input className="form-input" type="text" id="input-name" value={fling.name} />
|
||||
|
||||
<label className="form-label" htmlFor="input-share-url">Share</label>
|
||||
<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 className="form-group">
|
||||
<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>
|
||||
|
||||
<label className="form-switch form-inline">
|
||||
<input type="checkbox" id="allow-upload" checked={fling.sharing.allowUpload} onChange={toggleSharing}/>
|
||||
<i className="form-icon" /> Allow Uploads
|
||||
</label>
|
||||
|
||||
<label className="form-switch form-inline">
|
||||
<input type="checkbox" id="shared" checked={fling.sharing.shared} onChange={toggleSharing}/>
|
||||
<i className="form-icon" /> Shared
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<input type="submit" className="btn float-right" value="Save" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
0
web/fling/src/components/Settings.scss
Normal file
198
web/fling/src/components/Upload.jsx
Normal file
|
@ -0,0 +1,198 @@
|
|||
import log from 'loglevel';
|
||||
import React, {useState, useEffect, useRef} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {artifactClient} from '../util/flingclient';
|
||||
|
||||
import upload from './upload.svg';
|
||||
import drop from './drop.svg';
|
||||
import './Upload.scss';
|
||||
|
||||
|
||||
export default function Upload(props) {
|
||||
let fileInputRef = useRef(null);
|
||||
let [files, setFiles] = useState([]);
|
||||
let [dragging, setDragging] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("dragover",function(e){
|
||||
e.preventDefault();
|
||||
},false);
|
||||
window.addEventListener("drop",function(e){
|
||||
e.preventDefault();
|
||||
},false);
|
||||
});
|
||||
|
||||
function fileList() {
|
||||
let fileList = [];
|
||||
|
||||
files.forEach((file,idx) => {
|
||||
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()}</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);
|
||||
}
|
||||
|
||||
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);
|
||||
setDragging(true);
|
||||
}
|
||||
|
||||
function handleOnDragLeave(ev) {
|
||||
stopEvent(ev);
|
||||
setDragging(false);
|
||||
}
|
||||
|
||||
function stopEvent(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
function logFiles() {
|
||||
log.info("Files so far: ["+files.map((i) => i.name).join(',')+"]");
|
||||
}
|
||||
|
||||
function handleUpload() {
|
||||
for(let file of files) {
|
||||
artifactClient.postArtifact(props.activeFling, file);
|
||||
}
|
||||
}
|
||||
|
||||
function zoneContent(dragging) {
|
||||
if(dragging){
|
||||
return(
|
||||
<>
|
||||
<img className="dropzone-icon" alt="dropzone icon" src={drop}
|
||||
onDragOver={stopEvent} onDragLeave={stopEvent} />
|
||||
<h5 className="text-primary"
|
||||
onDragOver={stopEvent} onDragLeave={stopEvent}>
|
||||
Drop now!
|
||||
</h5>
|
||||
</>
|
||||
);
|
||||
}else {
|
||||
return(
|
||||
<>
|
||||
<img className="dropzone-icon-upload" alt="dropzone icon" src={upload}
|
||||
onDragOver={stopEvent} onDragLeave={stopEvent} />
|
||||
<h5 onDragOver={stopEvent} onDragLeave={stopEvent}>
|
||||
Click or Drop
|
||||
</h5>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return(
|
||||
<div className="container">
|
||||
{logFiles()}
|
||||
<div className="columns">
|
||||
<div className="column col-4 pl-0"
|
||||
onDrop={handleDrop}
|
||||
onClick={handleClick}
|
||||
onDragOver={stopEvent}
|
||||
onDragEnter={handleOnDragEnter}
|
||||
onDragLeave={handleOnDragLeave}>
|
||||
|
||||
<div className="dropzone c-hand py-2">
|
||||
<input className="d-hide" ref={fileInputRef} type="file" multiple
|
||||
onDragOver={stopEvent} onDragEnter={stopEvent} onDragLeave={stopEvent}
|
||||
onChange={handleFileInputChange} />
|
||||
{zoneContent(dragging)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="column col-8 pr-0" >
|
||||
<div className="file-list">
|
||||
<div className="row">
|
||||
<div className="container">
|
||||
<div className="columns">
|
||||
{fileList()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<span className="total-upload">Total Size: {totalSize()}</span>
|
||||
<button className="btn btn-primary btn-upload" onClick={handleUpload}>Upload</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
54
web/fling/src/components/Upload.scss
Normal file
|
@ -0,0 +1,54 @@
|
|||
@import "../styles/base.scss";
|
||||
@import "~spectre.css/src/mixins/_clearfix.scss";
|
||||
@import "~spectre.css/src/mixins/_position.scss";
|
||||
@import "~spectre.css/src/_variables.scss";
|
||||
@import "~spectre.css/src/_layout.scss";
|
||||
@import "~spectre.css/src/utilities/_position.scss";
|
||||
|
||||
.dropzone {
|
||||
@extend %shadow;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dropzone-icon {
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
.dropzone-icon-upload {
|
||||
@extend .dropzone-icon;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
@extend %shadow;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.file-list .row {
|
||||
@extend .container;
|
||||
@extend .my-2;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.total-upload {
|
||||
float: left;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.btn-upload {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.my-input {
|
||||
inset: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
74
web/fling/src/components/drop.svg
Normal file
|
@ -0,0 +1,74 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:version="1.0rc1 (09960d6f05, 2020-04-09)"
|
||||
sodipodi:docname="drop.svg"
|
||||
id="svg6"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
inkscape:current-layer="svg6"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-x="27"
|
||||
inkscape:cy="19.451453"
|
||||
inkscape:cx="6.1012061"
|
||||
inkscape:zoom="24.719275"
|
||||
inkscape:guide-bbox="true"
|
||||
showguides="true"
|
||||
showgrid="false"
|
||||
id="namedview8"
|
||||
inkscape:window-height="1052"
|
||||
inkscape:window-width="1893"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
guidetolerance="10"
|
||||
gridtolerance="10"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff">
|
||||
<sodipodi:guide
|
||||
id="guide852"
|
||||
orientation="1,0"
|
||||
position="11.997272,16.650509" />
|
||||
</sodipodi:namedview>
|
||||
<path
|
||||
style="fill:#5755d9;fill-opacity:1"
|
||||
sodipodi:nodetypes="cscssssc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4"
|
||||
d="M 19.35,10.04 C 18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14 c 0,3.31 2.69,6 6,6 h 13 c 2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96 z" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:1"
|
||||
sodipodi:nodetypes="cccccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
d="M 9.997272,11.931343 V 7.9313431 h 4.000001 v 3.9999999 h 3 l -5.000001,5.000001 -5,-5.000001 z"
|
||||
id="path4-3" />
|
||||
<path
|
||||
id="path2"
|
||||
fill="none"
|
||||
d="M0 0h24v24H0z" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
150
web/fling/src/components/send.svg
Normal file
|
@ -0,0 +1,150 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
height="271.42801"
|
||||
width="355.30499"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 355.305 271.42802"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="send.svg"
|
||||
inkscape:version="1.0rc1 (09960d6f05, 2020-04-09)"><metadata
|
||||
id="metadata981"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs979" /><sodipodi:namedview
|
||||
fit-margin-bottom="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-top="0"
|
||||
inkscape:document-rotation="0"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1893"
|
||||
inkscape:window-height="1052"
|
||||
id="namedview977"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.2029149"
|
||||
inkscape:cx="86.982498"
|
||||
inkscape:cy="-41.96885"
|
||||
inkscape:window-x="27"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1" />
|
||||
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#4693d2;fill-opacity:1"
|
||||
d="M 355.305,0 0,122.262 108.228,143.849 355.303,0 Z"
|
||||
id="path928" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#6babe0;fill-opacity:0.74902"
|
||||
d="m 0,122.262 108.228,21.587 71.829,-41.819 z"
|
||||
id="path930" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#0a62ad;fill-opacity:1"
|
||||
d="M 108.228,143.849 124.993,271.427 355.303,0 Z"
|
||||
id="path932" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#277cc3;fill-opacity:0.85098"
|
||||
d="m 108.228,143.849 16.765,127.578 102.937,-121.314 -17.769,-28.952 -35.202,29.386 5.1,-48.519 h -0.002 l -71.829,41.819 z"
|
||||
id="path934" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#fecd0d"
|
||||
d="m 355.305,0 -208.21,173.809 -22.099,97.619 z"
|
||||
id="path936" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#6babe0;fill-opacity:0.74902"
|
||||
d="m 174.96,150.548 -27.865,23.261 -22.099,97.619 102.937,-121.314 -17.769,-28.952 -35.202,29.386 v -0.002 z"
|
||||
id="path938" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#4693d2;fill-opacity:1"
|
||||
d="M 284.478,242.264 355.305,0 147.095,173.809 284.48,242.264 Z"
|
||||
id="path940" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#6babe0;fill-opacity:0.752941"
|
||||
d="m 174.803,150.548 -27.865,23.261 137.385,68.453 -74.314,-121.1 -35.207,29.388 v -0.002 z"
|
||||
id="path942" />
|
||||
|
||||
<g
|
||||
transform="translate(-70.023046,-144.17507)"
|
||||
id="g946">
|
||||
</g>
|
||||
<g
|
||||
transform="translate(-70.023046,-144.17507)"
|
||||
id="g948">
|
||||
</g>
|
||||
<g
|
||||
transform="translate(-70.023046,-144.17507)"
|
||||
id="g950">
|
||||
</g>
|
||||
<g
|
||||
transform="translate(-70.023046,-144.17507)"
|
||||
id="g952">
|
||||
</g>
|
||||
<g
|
||||
transform="translate(-70.023046,-144.17507)"
|
||||
id="g954">
|
||||
</g>
|
||||
<g
|
||||
transform="translate(-70.023046,-144.17507)"
|
||||
id="g956">
|
||||
</g>
|
||||
<g
|
||||
transform="translate(-70.023046,-144.17507)"
|
||||
id="g958">
|
||||
</g>
|
||||
<g
|
||||
transform="translate(-70.023046,-144.17507)"
|
||||
id="g960">
|
||||
</g>
|
||||
<g
|
||||
transform="translate(-70.023046,-144.17507)"
|
||||
id="g962">
|
||||
</g>
|
||||
<g
|
||||
transform="translate(-70.023046,-144.17507)"
|
||||
id="g964">
|
||||
</g>
|
||||
<g
|
||||
transform="translate(-70.023046,-144.17507)"
|
||||
id="g966">
|
||||
</g>
|
||||
<g
|
||||
transform="translate(-70.023046,-144.17507)"
|
||||
id="g968">
|
||||
</g>
|
||||
<g
|
||||
transform="translate(-70.023046,-144.17507)"
|
||||
id="g970">
|
||||
</g>
|
||||
<g
|
||||
transform="translate(-70.023046,-144.17507)"
|
||||
id="g972">
|
||||
</g>
|
||||
<g
|
||||
transform="translate(-70.023046,-144.17507)"
|
||||
id="g974">
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4 KiB |
1
web/fling/src/components/upload.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"/></svg>
|
After Width: | Height: | Size: 318 B |
15
web/fling/src/styles/base.scss
Normal file
|
@ -0,0 +1,15 @@
|
|||
@import "~normalize.css/normalize.css";
|
||||
|
||||
@import "~spectre.css/dist/spectre.min.css";
|
||||
@import "~spectre.css/dist/spectre-icons.css";
|
||||
|
||||
@import "partials/_shadows.scss";
|
||||
@import "variables/_breakpoints.scss";
|
||||
@import "variables/_colors.scss";
|
||||
|
||||
@import "form.scss";
|
||||
@import "container.scss";
|
||||
|
||||
body {
|
||||
background-color: #ebeced;
|
||||
}
|
8
web/fling/src/styles/container.scss
Normal file
|
@ -0,0 +1,8 @@
|
|||
/* Center within viewport */
|
||||
.container-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
17
web/fling/src/styles/form.scss
Normal file
|
@ -0,0 +1,17 @@
|
|||
@import "variables/_colors.scss";
|
||||
@import "variables/_sizes.scss";
|
||||
@import "partials/_shadows.scss";
|
||||
|
||||
.form-input {
|
||||
@extend %shadow;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.form-input-error {
|
||||
border-color: $red;
|
||||
}
|
||||
|
||||
.form-error-text {
|
||||
color: $red;
|
||||
font-size: $font-size-xs;
|
||||
}
|
30
web/fling/src/styles/partials/_shadows.scss
Normal file
|
@ -0,0 +1,30 @@
|
|||
%shadow-xs {
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
%shadow-sm {
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
%shadow {
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
%shadow-md {
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
%shadow-lg {
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
%shadow-xl {
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
%shadow-2xl {
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
%shadow-inner {
|
||||
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
%shadow-outline {
|
||||
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
|
||||
}
|
||||
%shadow-none {
|
||||
box-shadow: none;
|
||||
}
|
4
web/fling/src/styles/variables/_breakpoints.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
$sm: '640px';
|
||||
$md: '768px';
|
||||
$lg: '1024px';
|
||||
$xl: '1280px';
|
4
web/fling/src/styles/variables/_colors.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
$grey: #718096;
|
||||
$light-grey: #cbd5e0;
|
||||
|
||||
$red: #e53e3e;
|
4
web/fling/src/styles/variables/_sizes.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
$font-size-base: 1rem; // Assumes the browser default, typically `16px`
|
||||
$font-size-lg: $font-size-base * 1.25;
|
||||
$font-size-sm: $font-size-base * .875;
|
||||
$font-size-xs: $font-size-base * .75;
|