initial commit
commit
dde98003b0
|
@ -0,0 +1,44 @@
|
|||
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
ui/dist
|
||||
ui/tmp
|
||||
ui/out-tsc
|
||||
|
||||
# dependencies
|
||||
ui/node_modules
|
||||
|
||||
# profiling files
|
||||
ui/chrome-profiler-events.json
|
||||
ui/speed-measure-plugin.json
|
||||
|
||||
# IDEs and editors
|
||||
ui/.idea
|
||||
ui.project
|
||||
ui.classpath
|
||||
ui.c9/
|
||||
ui*.launch
|
||||
ui.settings/
|
||||
ui*.sublime-workspace
|
||||
|
||||
|
||||
|
||||
# misc
|
||||
ui/.sass-cache
|
||||
ui/connect.lock
|
||||
ui/coverage
|
||||
ui/libpeerconnection.log
|
||||
uinpm-debug.log
|
||||
uiyarn-error.log
|
||||
uitestem.log
|
||||
ui/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
app/.mvn
|
||||
app/.settings/
|
||||
app/.classpath
|
||||
app/.project
|
|
@ -0,0 +1,15 @@
|
|||
# Getting Started
|
||||
|
||||
### Reference Documentation
|
||||
For further reference, please consider the following sections:
|
||||
|
||||
* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)
|
||||
|
||||
### Guides
|
||||
The following guides illustrate how to use some features concretely:
|
||||
|
||||
* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/)
|
||||
* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/)
|
||||
* [Building REST services with Spring](https://spring.io/guides/tutorials/bookmarks/)
|
||||
* [Using WebSocket to build an interactive web application](https://spring.io/guides/gs/messaging-stomp-websocket/)
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
#!/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.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Maven2 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)`"
|
||||
# TODO classpath?
|
||||
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
|
||||
jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"
|
||||
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 command -v wget > /dev/null; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found wget ... using wget"
|
||||
fi
|
||||
wget "$jarUrl" -O "$wrapperJarPath"
|
||||
elif command -v curl > /dev/null; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found curl ... using curl"
|
||||
fi
|
||||
curl -o "$wrapperJarPath" "$jarUrl"
|
||||
else
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Falling back to using Java to download"
|
||||
fi
|
||||
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
||||
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
|
||||
|
||||
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 "$@"
|
|
@ -0,0 +1,161 @@
|
|||
@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 Maven2 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 key stroke 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 my 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.4.2/maven-wrapper-0.4.2.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% (
|
||||
echo Found %WRAPPER_JAR%
|
||||
) else (
|
||||
echo Couldn't find %WRAPPER_JAR%, downloading it ...
|
||||
echo Downloading from: %DOWNLOAD_URL%
|
||||
powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"
|
||||
echo Finished downloading %WRAPPER_JAR%
|
||||
)
|
||||
@REM End of extension
|
||||
|
||||
%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%
|
|
@ -0,0 +1,151 @@
|
|||
<?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 http://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.1.5.RELEASE</version>
|
||||
<relativePath /> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>webshell</groupId>
|
||||
<artifactId>app</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>app</name>
|
||||
<description>Demo project for Spring Boot</description>
|
||||
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.pty4j</groupId>
|
||||
<artifactId>pty4j</artifactId>
|
||||
<version>0.9.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- <dependency> -->
|
||||
<!-- <groupId>org.jetbrains.pty4j</groupId> -->
|
||||
<!-- <artifactId>purejavacomm</artifactId> -->
|
||||
<!-- <version>0.0.11.1</version> -->
|
||||
<!-- </dependency> -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.10.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.10.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.10.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.jcraft</groupId>
|
||||
<artifactId>jsch</artifactId>
|
||||
<version>0.1.54</version>
|
||||
</dependency>
|
||||
<!-- <dependency> -->
|
||||
<!-- <groupId>net.java.dev.jna</groupId> -->
|
||||
<!-- <artifactId>jna-platform</artifactId> -->
|
||||
<!-- <version>5.3.1</version> -->
|
||||
<!-- </dependency> -->
|
||||
<!-- <dependency> -->
|
||||
<!-- <groupId>net.java.dev.jna</groupId> -->
|
||||
<!-- <artifactId>jna</artifactId> -->
|
||||
<!-- <version>5.3.1</version> -->
|
||||
<!-- </dependency> -->
|
||||
<!-- <dependency> -->
|
||||
<!-- <groupId>log4j</groupId> -->
|
||||
<!-- <artifactId>log4j</artifactId> -->
|
||||
<!-- <version>1.2.17</version> -->
|
||||
<!-- </dependency> -->
|
||||
<!-- <dependency> -->
|
||||
<!-- <groupId>com.google.guava</groupId> -->
|
||||
<!-- <artifactId>guava</artifactId> -->
|
||||
<!-- <version>27.0-jre</version> -->
|
||||
<!-- </dependency> -->
|
||||
<!-- <dependency> -->
|
||||
<!-- <groupId>log4j</groupId> -->
|
||||
<!-- <artifactId>log4j</artifactId> -->
|
||||
<!-- <version>1.2.17</version> -->
|
||||
<!-- </dependency> -->
|
||||
<!-- https://mvnrepository.com/artifact/org.jetbrains/annotations -->
|
||||
<!-- <dependency> -->
|
||||
<!-- <groupId>org.jetbrains</groupId> -->
|
||||
<!-- <artifactId>annotations</artifactId> -->
|
||||
<!-- <version>16.0.3</version> -->
|
||||
<!-- </dependency> -->
|
||||
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
<version>1.62</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk15on -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk15on</artifactId>
|
||||
<version>1.62</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.tika</groupId>
|
||||
<artifactId>tika-core</artifactId>
|
||||
<version>1.21</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>bintray-jetbrains-pty4j</id>
|
||||
<name>bintray</name>
|
||||
<url>https://jetbrains.bintray.com/pty4j</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package webshell.app;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import webshell.app.terminal.PtySession;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
|
||||
public class AppContext {
|
||||
public static final Map<String, PtySession> INSTANCES = new ConcurrentHashMap<>();
|
||||
public static final Map<String, PtySession> SESSION_MAP = new ConcurrentHashMap<>();
|
||||
}
|
|
@ -0,0 +1,312 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package webshell.app;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
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.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import webshell.app.config.ConfigManager;
|
||||
import webshell.app.files.FileOperations;
|
||||
import webshell.app.files.FileService;
|
||||
import webshell.app.files.FileTransfer;
|
||||
import webshell.app.files.PosixPermission;
|
||||
import webshell.app.files.copy.FileCopyProgressResponse;
|
||||
import webshell.app.files.copy.FileCopyRequest;
|
||||
import webshell.app.files.search.SearchResult;
|
||||
import webshell.app.terminal.PtySession;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
@RestController
|
||||
@CrossOrigin(allowCredentials = "true")
|
||||
@RequestMapping("/api")
|
||||
public class AppController {
|
||||
|
||||
@Autowired
|
||||
private FileService service;
|
||||
|
||||
@Autowired
|
||||
private FileOperations fs;
|
||||
|
||||
@Autowired
|
||||
private BCryptPasswordEncoder passwordEncoder;
|
||||
|
||||
@PostMapping("/app/terminal/{appId}/resize")
|
||||
public void resizePty(@PathVariable String appId,
|
||||
@RequestBody Map<String, Integer> body) {
|
||||
AppContext.INSTANCES.get(appId).resizePty(body.get("row"),
|
||||
body.get("col"));
|
||||
}
|
||||
|
||||
@PostMapping("/app/terminal")
|
||||
public Map<String, String> createTerminal() throws Exception {
|
||||
PtySession pty = new PtySession();
|
||||
AppContext.INSTANCES.put(pty.getId(), pty);
|
||||
Map<String, String> map = new HashMap<String, String>();
|
||||
map.put("id", pty.getId());
|
||||
return map;
|
||||
}
|
||||
|
||||
@GetMapping("/app/files/home")
|
||||
public Map<String, Object> listHome() throws Exception {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("files", service.list(System.getProperty("user.home")));
|
||||
map.put("folder", System.getProperty("user.home"));
|
||||
map.put("folderName",
|
||||
new File(System.getProperty("user.home")).getName());
|
||||
return map;
|
||||
}
|
||||
|
||||
@GetMapping("/app/files/list/{path}")
|
||||
public Map<String, Object> list(@PathVariable String path)
|
||||
throws Exception {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
String folder = null;
|
||||
if (path == null) {
|
||||
folder = System.getProperty("user.home");
|
||||
} else {
|
||||
folder = new String(Base64.getDecoder().decode(path), "utf-8");
|
||||
}
|
||||
System.out.println("base64: " + path + " " + folder);
|
||||
map.put("files", service.list(folder));
|
||||
map.put("folder", folder);
|
||||
map.put("folderName", new File(folder).getName());
|
||||
return map;
|
||||
}
|
||||
|
||||
@GetMapping("/app/files/up/{path}")
|
||||
public Map<String, Object> up(@PathVariable String path) throws Exception {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
path = new String(Base64.getDecoder().decode(path), "utf-8");
|
||||
if (!path.equals("/")) {
|
||||
path = new File(path).getParent();
|
||||
}
|
||||
map.put("files", service.list(path));
|
||||
map.put("folder", path);
|
||||
map.put("folderName", new File(path).getName());
|
||||
return map;
|
||||
}
|
||||
|
||||
@PostMapping("/app/upload/{folder}/{relativePath}")
|
||||
public void upload(@PathVariable String folder,
|
||||
@PathVariable String relativePath, HttpServletRequest request)
|
||||
throws Exception {
|
||||
System.out.println("uploading...");
|
||||
try {
|
||||
folder = new String(Base64.getDecoder().decode(folder), "utf-8");
|
||||
relativePath = new String(Base64.getDecoder().decode(relativePath),
|
||||
"utf-8");
|
||||
FileTransfer fs = new FileTransfer(false);
|
||||
fs.transferFile(relativePath, folder, request.getInputStream());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/app/folder/tree/home")
|
||||
public List<Map<String, ?>> listTreeHome() {
|
||||
List<Map<String, ?>> list = new ArrayList<>();
|
||||
File f = new File(System.getProperty("user.home"));
|
||||
File[] files = f.listFiles();
|
||||
if (files != null && files.length > 0) {
|
||||
for (File file : files) {
|
||||
Map<String, Object> entry = new HashMap<>();
|
||||
entry.put("name", file.getName());
|
||||
entry.put("path", file.getAbsolutePath());
|
||||
entry.put("leafNode", !file.isDirectory());
|
||||
list.add(entry);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@GetMapping("/app/folder/tree/path/{encodedPath}")
|
||||
public List<Map<String, ?>> listTreePath(@PathVariable String encodedPath)
|
||||
throws Exception {
|
||||
String path = new String(Base64.getDecoder().decode(encodedPath),
|
||||
"utf-8");
|
||||
List<Map<String, ?>> list = new ArrayList<>();
|
||||
File f = new File(path);
|
||||
File[] files = f.listFiles();
|
||||
if (files != null && files.length > 0) {
|
||||
for (File file : files) {
|
||||
Map<String, Object> entry = new HashMap<>();
|
||||
entry.put("name", file.getName());
|
||||
entry.put("path", file.getAbsolutePath());
|
||||
entry.put("leafNode", !file.isDirectory());
|
||||
list.add(entry);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@GetMapping("/app/folder/tree/fs")
|
||||
public List<Map<String, ?>> listFsRoots() throws Exception {
|
||||
List<Map<String, ?>> list = new ArrayList<>();
|
||||
File[] files = File.listRoots();
|
||||
if (files != null && files.length > 0) {
|
||||
for (File file : files) {
|
||||
Map<String, Object> entry = new HashMap<>();
|
||||
System.out.println("Root: " + file.getName());
|
||||
entry.put("name", file.getAbsolutePath());
|
||||
entry.put("path", file.getAbsolutePath());
|
||||
entry.put("leafNode", !file.isDirectory());
|
||||
list.add(entry);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@PostMapping("/app/fs/{mode}")
|
||||
public Map<String, String> startCopyOrMove(@PathVariable String mode,
|
||||
@RequestBody FileCopyRequest request) {
|
||||
List<String> sourceFile = request.getSourceFile();
|
||||
String targetFolder = request.getTargetFolder();
|
||||
Map<String, String> map = new HashMap<>();
|
||||
String id = fs.createFileCopyTask(sourceFile, targetFolder,
|
||||
"move".equals(mode));
|
||||
map.put("id", id);
|
||||
map.put("name", fs.getOpName(id));
|
||||
return map;
|
||||
}
|
||||
|
||||
@PostMapping("/app/fs/progress")
|
||||
public List<FileCopyProgressResponse> getProgress(
|
||||
@RequestBody List<String> idList) {
|
||||
return fs.getProgress(idList);
|
||||
}
|
||||
|
||||
@PostMapping("/app/fs/cancel/{id}")
|
||||
public void cancel(@PathVariable String id) {
|
||||
fs.cancelTask(id);
|
||||
}
|
||||
|
||||
@PostMapping("/app/fs/rename")
|
||||
public void rename(@RequestBody Map<String, String> body)
|
||||
throws IOException {
|
||||
fs.rename(body.get("oldName"), body.get("newName"), body.get("folder"));
|
||||
}
|
||||
|
||||
@PostMapping("/app/fs/delete")
|
||||
public void delete(@RequestBody List<String> body) throws IOException {
|
||||
fs.deleteFiles(body);
|
||||
}
|
||||
|
||||
@GetMapping("/app/fs/files/{encodedPath}")
|
||||
public String getText(@PathVariable String encodedPath) throws Exception {
|
||||
return service
|
||||
.getText(new String(Base64.getDecoder().decode(encodedPath)));
|
||||
}
|
||||
|
||||
@PostMapping("/app/fs/files/{encodedPath}")
|
||||
public void setText(@PathVariable String encodedPath,
|
||||
@RequestBody String body) throws Exception {
|
||||
service.setText(new String(Base64.getDecoder().decode(encodedPath)),
|
||||
body);
|
||||
}
|
||||
|
||||
@PostMapping("/app/fs/search")
|
||||
public Map<String, String> initSearch(
|
||||
@RequestBody Map<String, String> request) {
|
||||
String id = service.createSearch(request.get("folder"),
|
||||
request.get("searchText"));
|
||||
Map<String, String> response = new HashMap<String, String>();
|
||||
response.put("id", id);
|
||||
return response;
|
||||
}
|
||||
|
||||
@DeleteMapping("/app/fs/search/{id}")
|
||||
public void cancelSearch(@PathVariable String id) {
|
||||
this.service.cancelSearch(id);
|
||||
}
|
||||
|
||||
@GetMapping("/app/fs/search/{id}")
|
||||
public SearchResult getSearchResult(@PathVariable String id,
|
||||
@RequestParam(defaultValue = "0", required = false) int fileIndex,
|
||||
@RequestParam(defaultValue = "0", required = false) int folderIndex) {
|
||||
return this.service.getSearchResult(id, fileIndex, folderIndex);
|
||||
}
|
||||
|
||||
@GetMapping("/app/fs/posix/{encodedPath}")
|
||||
public PosixPermission getPosixPerm(@PathVariable String encodedPath)
|
||||
throws Exception {
|
||||
return this.fs.getPosixPermission(
|
||||
new String(Base64.getDecoder().decode(encodedPath), "utf-8"));
|
||||
}
|
||||
|
||||
@PostMapping("/app/fs/posix/{encodedPath}")
|
||||
public void setPosixPerm(@PathVariable String encodedPath,
|
||||
@RequestBody PosixPermission perm) throws Exception {
|
||||
this.fs.setPosixPermission(
|
||||
new String(Base64.getDecoder().decode(encodedPath), "utf-8"),
|
||||
perm);
|
||||
}
|
||||
|
||||
@GetMapping("/app/config")
|
||||
public Map<String, String> getConfig() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("app.default-user", System.getProperty("app.default-user"));
|
||||
map.put("app.default-pass", System.getProperty("app.default-pass"));
|
||||
map.put("app.default-shell", System.getProperty("app.default-shell"));
|
||||
return map;
|
||||
}
|
||||
|
||||
@PostMapping("/app/config")
|
||||
public void setConfig(@RequestBody Map<String, String> map)
|
||||
throws IOException {
|
||||
for (String key : map.keySet()) {
|
||||
String val = map.get(key);
|
||||
if (val != null && val.length() > 0) {
|
||||
if (key.equals("app.default-pass")) {
|
||||
System.setProperty(key, passwordEncoder.encode(val));
|
||||
} else {
|
||||
System.setProperty(key, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
ConfigManager.saveUserDetails();
|
||||
}
|
||||
|
||||
@PostMapping("/app/fs/mkdir")
|
||||
public void mkdir(@RequestBody Map<String, String> map) throws Exception {
|
||||
String dir = map.get("dir");
|
||||
String name = map.get("name");
|
||||
Path path = Paths.get(dir, name);
|
||||
Files.createDirectories(path);
|
||||
}
|
||||
|
||||
@PostMapping("/app/fs/touch")
|
||||
public void touch(@RequestBody Map<String, String> map) throws Exception {
|
||||
String dir = map.get("dir");
|
||||
String name = map.get("name");
|
||||
Path path = Paths.get(dir, name);
|
||||
Files.createFile(path);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
package webshell.app;
|
||||
|
||||
import java.security.Security;
|
||||
import java.util.Collections;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
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.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import webshell.app.config.ConfigManager;
|
||||
import webshell.app.security.CustomAuthEntryPoint;
|
||||
import webshell.app.security.JwtAuthorizationFilter;
|
||||
import webshell.app.terminal.TerminalWebsocketHandler;
|
||||
|
||||
@EnableWebSecurity
|
||||
@EnableWebSocket
|
||||
@SpringBootApplication
|
||||
public class Application extends WebSecurityConfigurerAdapter
|
||||
implements WebSocketConfigurer, WebMvcConfigurer, CommandLineRunner {
|
||||
|
||||
@Autowired
|
||||
private Environment env;
|
||||
|
||||
static {
|
||||
// adds the Bouncy castle provider to java security
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
ConfigManager.checkAndConfigureSSL();
|
||||
}
|
||||
|
||||
public static final SecretKey SECRET_KEY = Keys
|
||||
.secretKeyFor(SignatureAlgorithm.HS256);
|
||||
|
||||
@Autowired
|
||||
private CustomAuthEntryPoint auth;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||
System.out.println("Encoded: " + passwordEncoder().encode("admin"));
|
||||
System.out.println("Registered ws");
|
||||
registry.addHandler(new TerminalWebsocketHandler(), "/term*")
|
||||
.setAllowedOrigins("*");// .withSockJS();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http.httpBasic().authenticationEntryPoint(auth).and().cors().and()
|
||||
.csrf().disable().authorizeRequests().antMatchers("/term**")
|
||||
.permitAll().and().authorizeRequests().antMatchers("/bin/**")
|
||||
.permitAll().and().authorizeRequests().antMatchers("/api/**")
|
||||
.authenticated().and().authorizeRequests().antMatchers("/**")
|
||||
.permitAll().and()
|
||||
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
|
||||
.sessionManagement()
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BCryptPasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(AuthenticationManagerBuilder auth) throws Exception {
|
||||
|
||||
AuthenticationProvider provider = new AuthenticationProvider() {
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return authentication == (UsernamePasswordAuthenticationToken.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication)
|
||||
throws AuthenticationException {
|
||||
System.out.println("Authenticating...");
|
||||
String user = authentication.getName();
|
||||
String pass = authentication.getCredentials().toString();
|
||||
System.out.println(
|
||||
user + "--" + System.getProperty("app.default-user"));
|
||||
if (user.equals(System.getProperty("app.default-user"))
|
||||
&& passwordEncoder().matches(pass,
|
||||
System.getProperty("app.default-pass"))) {
|
||||
return new UsernamePasswordAuthenticationToken(user, pass,
|
||||
Collections.emptyList());
|
||||
} else {
|
||||
throw new BadCredentialsException(
|
||||
"External system authentication failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
auth.authenticationProvider(provider);
|
||||
// auth.authenticationProvider(new UserDetailsService() {
|
||||
//
|
||||
// @Override
|
||||
// public UserDetails loadUserByUsername(String username)
|
||||
// throws UsernameNotFoundException {
|
||||
// // TODO Auto-generated method stub
|
||||
// return null;
|
||||
// }
|
||||
// });
|
||||
// auth.inMemoryAuthentication()
|
||||
// .withUser(System.getProperty("app.default-user"))
|
||||
// .password("{noop}" + System.getProperty("app.default-pass"))
|
||||
// .authorities("ROLE_USER");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(String... args) throws Exception {
|
||||
ConfigManager.loadUserDetails(env);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package webshell.app;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
@Controller
|
||||
public class SinglePageAppController {
|
||||
@RequestMapping(value = { "/", "/app/**", "/login" })
|
||||
public String index() {
|
||||
return "/index.html";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
package webshell.app.config;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Date;
|
||||
import java.util.Properties;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.OperatorCreationException;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
|
||||
public class ConfigManager {
|
||||
|
||||
private static Path CONFIG_PATH = null;
|
||||
|
||||
private static final String CERTIFICATE_ALIAS = "webshell-cert";
|
||||
private static final String CERTIFICATE_ALGORITHM = "RSA";
|
||||
private static final String CERTIFICATE_DN = "CN=easy.webshell.app, O=easy.webshell.app, L=easy.webshell.app, ST=il, C=c";
|
||||
private static final int CERTIFICATE_BITS = 2048;
|
||||
|
||||
static {
|
||||
Path configPath = null;
|
||||
String configPathStr = System.getenv("EASY_WEB_SHELL_CONFIG_DIR");
|
||||
if (configPathStr == null || configPathStr.length() < 1) {
|
||||
configPath = Paths.get(System.getProperty("user.home"),
|
||||
".easy-web-shell");
|
||||
} else {
|
||||
configPath = Paths.get(configPathStr, ".easy-web-shell");
|
||||
}
|
||||
CONFIG_PATH = configPath;
|
||||
try {
|
||||
Files.createDirectories(configPath);
|
||||
} catch (FileAlreadyExistsException e) {
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
System.exit(-1);
|
||||
}
|
||||
|
||||
// loadUserDetails();
|
||||
}
|
||||
|
||||
public static void saveUserDetails()
|
||||
throws FileNotFoundException, IOException {
|
||||
Path userConfigPath = CONFIG_PATH.resolve("users.properties");
|
||||
Properties prop = new Properties();
|
||||
prop.put("app.default-user", System.getProperty("app.default-user"));
|
||||
prop.put("app.default-pass", System.getProperty("app.default-pass"));
|
||||
prop.put("app.default-shell", System.getProperty("app.default-shell"));
|
||||
|
||||
try (OutputStream out = new FileOutputStream(userConfigPath.toFile())) {
|
||||
prop.store(out, "User details");
|
||||
}
|
||||
}
|
||||
|
||||
public static void loadUserDetails(Environment env) {
|
||||
Path userConfigPath = CONFIG_PATH.resolve("users.properties");
|
||||
Properties prop = new Properties();
|
||||
|
||||
if (Files.exists(userConfigPath)) {
|
||||
try (InputStream in = new FileInputStream(
|
||||
userConfigPath.toFile())) {
|
||||
prop.load(in);
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (prop.size() < 1) {
|
||||
prop.put("app.default-user",
|
||||
env.getProperty("app.default-admin-user"));
|
||||
prop.put("app.default-pass",
|
||||
env.getProperty("app.default-admin-pass"));
|
||||
prop.put("app.default-shell", "auto");
|
||||
}
|
||||
|
||||
for (String propName : prop.stringPropertyNames()) {
|
||||
System.setProperty(propName, prop.getProperty(propName));
|
||||
}
|
||||
|
||||
// System.setProperty("app.default-user",
|
||||
// prop.getProperty("app.default-user"));
|
||||
// System.setProperty("app.default-pass",
|
||||
// prop.getProperty("app.default-pass"));
|
||||
// System.setProperty("app.default-shell", prop.getProperty("app.default-shell"));
|
||||
}
|
||||
|
||||
public static void checkAndConfigureSSL() {
|
||||
try {
|
||||
Path certPath = CONFIG_PATH.resolve("cert.p12");
|
||||
Path certConf = CONFIG_PATH.resolve("cert.properties");
|
||||
if (!Files.exists(certConf)) {
|
||||
createSelfSignedCertificate(certPath, certConf);
|
||||
}
|
||||
|
||||
System.out.println("Loading existing certificate");
|
||||
Properties certProp = new Properties();
|
||||
try (InputStream in = new FileInputStream(certConf.toFile())) {
|
||||
certProp.load(in);
|
||||
}
|
||||
|
||||
System.setProperty("server.ssl.key-store-password",
|
||||
certProp.getProperty("key-store-password"));
|
||||
System.setProperty("server.ssl.key-alias",
|
||||
certProp.getProperty("key-alias"));
|
||||
System.setProperty("server.ssl.key-store", certPath.toString());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
System.exit(-1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void createSelfSignedCertificate(Path certPath,
|
||||
Path certConf) throws IOException, NoSuchAlgorithmException,
|
||||
OperatorCreationException, CertificateException, KeyStoreException {
|
||||
String keystorePassword = UUID.randomUUID().toString();
|
||||
X509Certificate cert = null;
|
||||
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator
|
||||
.getInstance(CERTIFICATE_ALGORITHM);
|
||||
keyPairGenerator.initialize(CERTIFICATE_BITS, new SecureRandom());
|
||||
KeyPair keyPair = keyPairGenerator.generateKeyPair();
|
||||
|
||||
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(
|
||||
new X500Name(CERTIFICATE_DN),
|
||||
BigInteger.valueOf(System.currentTimeMillis()),
|
||||
new Date(System.currentTimeMillis() - 1000L * 60 * 60 * 24),
|
||||
new Date(System.currentTimeMillis()
|
||||
+ (1000L * 60 * 60 * 24 * 365 * 10)),
|
||||
new X500Name(CERTIFICATE_DN), SubjectPublicKeyInfo
|
||||
.getInstance(keyPair.getPublic().getEncoded()));
|
||||
|
||||
JcaContentSignerBuilder builder = new JcaContentSignerBuilder(
|
||||
"SHA256withRSA");
|
||||
ContentSigner signer = builder.build(keyPair.getPrivate());
|
||||
|
||||
byte[] certBytes = certBuilder.build(signer).getEncoded();
|
||||
CertificateFactory certificateFactory = CertificateFactory
|
||||
.getInstance("X.509");
|
||||
cert = (X509Certificate) certificateFactory
|
||||
.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||
|
||||
KeyStore keyStore = KeyStore.getInstance("PKCS12");
|
||||
keyStore.load(null, null);
|
||||
keyStore.setKeyEntry(CERTIFICATE_ALIAS, keyPair.getPrivate(),
|
||||
keystorePassword.toCharArray(),
|
||||
new java.security.cert.Certificate[] { cert });
|
||||
|
||||
try (OutputStream out = new FileOutputStream(certPath.toFile())) {
|
||||
keyStore.store(out, keystorePassword.toCharArray());
|
||||
}
|
||||
|
||||
Properties certProps = new Properties();
|
||||
certProps.setProperty("key-store-password", keystorePassword);
|
||||
certProps.setProperty("key-alias", CERTIFICATE_ALIAS);
|
||||
certProps.setProperty("key-store", certPath.toString());
|
||||
|
||||
try (OutputStream out = new FileOutputStream(certConf.toFile())) {
|
||||
certProps.store(out,
|
||||
"Easy webshell self signed certificate details");
|
||||
}
|
||||
|
||||
System.out.println("Self signed certificate created");
|
||||
|
||||
// X509V3CertificateGenerator v3CertGen = new X509V3CertificateGenerator();
|
||||
// v3CertGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
|
||||
// v3CertGen.setIssuerDN(new X509Principal(CERTIFICATE_DN));
|
||||
// v3CertGen.setNotBefore(new Date(System.currentTimeMillis() - 1000L * 60 * 60 * 24));
|
||||
// v3CertGen.setNotAfter(new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 365 * 10)));
|
||||
// v3CertGen.setSubjectDN(new X509Principal(CERTIFICATE_DN));
|
||||
// v3CertGen.setPublicKey(keyPair.getPublic());
|
||||
// v3CertGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
|
||||
// cert = v3CertGen.generateX509Certificate(keyPair.getPrivate());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package webshell.app.files;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jws;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import webshell.app.Application;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
@RestController
|
||||
@CrossOrigin(allowCredentials = "true")
|
||||
@RequestMapping("/bin")
|
||||
public class BinaryDataController {
|
||||
|
||||
@Autowired
|
||||
private FileTypeDetector typeDetector;
|
||||
|
||||
private void validateToken(String token) throws Exception {
|
||||
Jws<Claims> parsedToken = Jwts.parser()
|
||||
.setSigningKey(Application.SECRET_KEY).parseClaimsJws(token);
|
||||
|
||||
String username = parsedToken.getBody().getSubject();
|
||||
|
||||
if (username != null && username.length() > 0
|
||||
&& username.equals(System.getProperty("app.default-user"))) {
|
||||
System.out.println("Token and user is valid");
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Exception("Invalid username: " + username);
|
||||
}
|
||||
|
||||
@GetMapping("/image/{encodedPath}")
|
||||
public ResponseEntity<byte[]> getImage(@PathVariable String encodedPath,
|
||||
@RequestParam String token) throws Exception {
|
||||
validateToken(token);
|
||||
String path = new String(Base64.getDecoder().decode(encodedPath));
|
||||
byte[] b = Files.readAllBytes(Paths.get(path));
|
||||
MultiValueMap<String, String> headers = new HttpHeaders();
|
||||
headers.add("Content-Type", getImageType(path));
|
||||
ResponseEntity<byte[]> resp = new ResponseEntity<byte[]>(b, headers,
|
||||
HttpStatus.OK);
|
||||
return resp;
|
||||
}
|
||||
|
||||
@GetMapping("/blob/{encodedPath}")
|
||||
public void getBlob(@PathVariable String encodedPath,
|
||||
@RequestParam String token, HttpServletRequest request,
|
||||
HttpServletResponse resp) throws Exception {
|
||||
validateToken(token);
|
||||
String path = new String(Base64.getDecoder().decode(encodedPath));
|
||||
System.out.println("Get file: " + path);
|
||||
long fileSize = Files.size(Paths.get(path));
|
||||
String r = request.getHeader("Range");
|
||||
long upperBound = fileSize - 1;
|
||||
long lowerBound = 0;
|
||||
if (r != null) {
|
||||
System.out.println(r);
|
||||
String rstr = r.split("=")[1];
|
||||
String arr[] = rstr.split("-");
|
||||
lowerBound = Long.parseLong(arr[0]);
|
||||
if (arr.length > 1 && arr[1].length() > 0) {
|
||||
upperBound = Long.parseLong(arr[1]);
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("lb: " + lowerBound + " ub: " + fileSize);
|
||||
|
||||
if (r != null) {
|
||||
resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||
}
|
||||
resp.addHeader("Content-Length", (fileSize - lowerBound) + "");
|
||||
resp.addHeader("Content-Range",
|
||||
"bytes " + lowerBound + "-" + upperBound + "/" + fileSize);// (fileSize
|
||||
// -
|
||||
// lowerBound)
|
||||
// +
|
||||
// "");
|
||||
resp.addHeader("Content-Type", typeDetector.getType(new File(path)));
|
||||
long rem = fileSize - lowerBound;
|
||||
try (InputStream in = new FileInputStream(path)) {
|
||||
in.skip(lowerBound);
|
||||
byte[] b = new byte[8192];
|
||||
while (rem > 0) {
|
||||
int x = in.read(b, 0, (int) (rem > b.length ? b.length : rem));
|
||||
if (x == -1)
|
||||
break;
|
||||
resp.getOutputStream().write(b, 0, x);
|
||||
rem -= x;
|
||||
}
|
||||
}
|
||||
// Files.copy(Paths.get(path), resp.getOutputStream());
|
||||
}
|
||||
|
||||
@GetMapping("/download")
|
||||
public void downloadFiles(
|
||||
@RequestParam(name = "folder") String encodedFolder,
|
||||
@RequestParam(name = "files", required = false) String encodedFiles,
|
||||
@RequestParam String token, HttpServletResponse response)
|
||||
throws Exception {
|
||||
validateToken(token);
|
||||
int fileCount = 0;
|
||||
|
||||
String folder = new String(Base64.getDecoder().decode(encodedFolder),
|
||||
"utf-8");
|
||||
|
||||
List<String> fileList = new ArrayList<>();
|
||||
|
||||
if (encodedFiles != null) {
|
||||
String files = new String(Base64.getDecoder().decode(encodedFiles),
|
||||
"utf-8");
|
||||
String[] arr1 = files.split("/");
|
||||
if (arr1 != null && arr1.length > 0) {
|
||||
fileCount = arr1.length;
|
||||
for (String str : arr1) {
|
||||
fileList.add(new File(folder, str).getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fileList.size() == 0) {
|
||||
throw new IOException();
|
||||
}
|
||||
|
||||
String name = "download.zip";
|
||||
|
||||
boolean compress = false;
|
||||
|
||||
if (fileCount == 1) {
|
||||
File f = new File(fileList.get(0));
|
||||
name = f.getName();
|
||||
compress = f.isDirectory();
|
||||
if (name.length() < 1) {
|
||||
name = "files";
|
||||
}
|
||||
} else {
|
||||
compress = true;
|
||||
}
|
||||
|
||||
response.addHeader("Content-Disposition",
|
||||
"attachment; filename=\"" + name + ".zip" + "\"");
|
||||
response.setContentType("application/octet-stream");
|
||||
FileTransfer fs = new FileTransfer(compress);
|
||||
fs.transferFiles(fileList, response.getOutputStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path
|
||||
* @return
|
||||
*/
|
||||
private String getImageType(String path) {
|
||||
return typeDetector.getTypeByName(path);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package webshell.app.files;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
public class FileInfo {
|
||||
|
||||
private String name, path, permissionString, user, type;
|
||||
private long size;
|
||||
private Date lastModified;
|
||||
private boolean posix;
|
||||
|
||||
/**
|
||||
* @param name
|
||||
* @param path
|
||||
* @param permissionString
|
||||
* @param user
|
||||
* @param type
|
||||
* @param size
|
||||
* @param permission
|
||||
* @param lastModified
|
||||
*/
|
||||
public FileInfo(String name, String path, String permissionString,
|
||||
String user, String type, long size, long permission,
|
||||
Date lastModified, boolean posix) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.path = path;
|
||||
this.permissionString = permissionString;
|
||||
this.user = user;
|
||||
this.type = type;
|
||||
this.size = size;
|
||||
this.lastModified = lastModified;
|
||||
this.posix = posix;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name the name to set
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the path
|
||||
*/
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path the path to set
|
||||
*/
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the permissionString
|
||||
*/
|
||||
public String getPermissionString() {
|
||||
return permissionString;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param permissionString the permissionString to set
|
||||
*/
|
||||
public void setPermissionString(String permissionString) {
|
||||
this.permissionString = permissionString;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the user
|
||||
*/
|
||||
public String getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param user the user to set
|
||||
*/
|
||||
public void setUser(String user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the type
|
||||
*/
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param type the type to set
|
||||
*/
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the size
|
||||
*/
|
||||
public long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param size the size to set
|
||||
*/
|
||||
public void setSize(long size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the lastModified
|
||||
*/
|
||||
public Date getLastModified() {
|
||||
return lastModified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param lastModified the lastModified to set
|
||||
*/
|
||||
public void setLastModified(Date lastModified) {
|
||||
this.lastModified = lastModified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the posix
|
||||
*/
|
||||
public boolean isPosix() {
|
||||
return posix;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param posix the posix to set
|
||||
*/
|
||||
public void setPosix(boolean posix) {
|
||||
this.posix = posix;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package webshell.app.files;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.PosixFileAttributeView;
|
||||
import java.nio.file.attribute.PosixFileAttributes;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import webshell.app.files.copy.FileCopyProgressResponse;
|
||||
import webshell.app.files.copy.FileCopyTask;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
public class FileOperations {
|
||||
private final Map<String, FileCopyTask> pendingOperations = new ConcurrentHashMap<>();
|
||||
private final ExecutorService threadPool = Executors.newFixedThreadPool(5);
|
||||
|
||||
public void setPosixPermission(String file, PosixPermission perm)
|
||||
throws Exception {
|
||||
Set<PosixFilePermission> permissions = Files
|
||||
.getPosixFilePermissions(Paths.get(file));
|
||||
|
||||
if (perm.isExecutable()) {
|
||||
permissions.add(PosixFilePermission.OWNER_EXECUTE);
|
||||
} else {
|
||||
permissions.remove(PosixFilePermission.OWNER_EXECUTE);
|
||||
}
|
||||
|
||||
if ("READ_WRITE".equals(perm.getOwnerAccess())) {
|
||||
permissions.add(PosixFilePermission.OWNER_READ);
|
||||
permissions.add(PosixFilePermission.OWNER_WRITE);
|
||||
} else if ("READ_ONLY".equals(perm.getOwnerAccess())) {
|
||||
permissions.add(PosixFilePermission.OWNER_READ);
|
||||
permissions.remove(PosixFilePermission.OWNER_WRITE);
|
||||
} else if ("WRITE_ONLY".equals(perm.getOwnerAccess())) {
|
||||
permissions.remove(PosixFilePermission.OWNER_READ);
|
||||
permissions.add(PosixFilePermission.OWNER_WRITE);
|
||||
}else {
|
||||
permissions.remove(PosixFilePermission.OWNER_READ);
|
||||
permissions.remove(PosixFilePermission.OWNER_WRITE);
|
||||
}
|
||||
|
||||
if ("READ_WRITE".equals(perm.getGroupAccess())) {
|
||||
permissions.add(PosixFilePermission.GROUP_READ);
|
||||
permissions.add(PosixFilePermission.GROUP_WRITE);
|
||||
} else if ("READ_ONLY".equals(perm.getGroupAccess())) {
|
||||
permissions.add(PosixFilePermission.GROUP_READ);
|
||||
permissions.remove(PosixFilePermission.GROUP_WRITE);
|
||||
} else if ("WRITE_ONLY".equals(perm.getGroupAccess())) {
|
||||
permissions.remove(PosixFilePermission.GROUP_READ);
|
||||
permissions.add(PosixFilePermission.GROUP_WRITE);
|
||||
}else {
|
||||
permissions.remove(PosixFilePermission.GROUP_READ);
|
||||
permissions.remove(PosixFilePermission.GROUP_WRITE);
|
||||
permissions.remove(PosixFilePermission.GROUP_EXECUTE);
|
||||
}
|
||||
|
||||
if ("READ_WRITE".equals(perm.getOtherAccess())) {
|
||||
permissions.add(PosixFilePermission.OTHERS_READ);
|
||||
permissions.add(PosixFilePermission.OTHERS_WRITE);
|
||||
} else if ("READ_ONLY".equals(perm.getOtherAccess())) {
|
||||
permissions.add(PosixFilePermission.OTHERS_READ);
|
||||
permissions.remove(PosixFilePermission.OTHERS_WRITE);
|
||||
} else if ("WRITE_ONLY".equals(perm.getOtherAccess())) {
|
||||
permissions.remove(PosixFilePermission.OTHERS_READ);
|
||||
permissions.add(PosixFilePermission.OTHERS_WRITE);
|
||||
}else {
|
||||
permissions.remove(PosixFilePermission.OTHERS_READ);
|
||||
permissions.remove(PosixFilePermission.OTHERS_WRITE);
|
||||
permissions.remove(PosixFilePermission.OTHERS_EXECUTE);
|
||||
}
|
||||
|
||||
Files.setPosixFilePermissions(Paths.get(file), permissions);
|
||||
}
|
||||
|
||||
public PosixPermission getPosixPermission(String file) throws Exception {
|
||||
Path path = Paths.get(file);
|
||||
PosixFileAttributeView view = Files.getFileAttributeView(path,
|
||||
PosixFileAttributeView.class);
|
||||
PosixFileAttributes attrs = view.readAttributes();
|
||||
PosixPermission perm = new PosixPermission();
|
||||
perm.setOwner(attrs.owner().getName());
|
||||
perm.setGroup(attrs.group().getName());
|
||||
Set<PosixFilePermission> permissions = attrs.permissions();
|
||||
perm.setExecutable(
|
||||
permissions.contains(PosixFilePermission.OWNER_EXECUTE));
|
||||
|
||||
String ownerPerm = "NONE";
|
||||
if (permissions.contains(PosixFilePermission.OWNER_READ)
|
||||
&& permissions.contains(PosixFilePermission.OWNER_WRITE)) {
|
||||
ownerPerm = "READ_WRITE";
|
||||
} else if (permissions.contains(PosixFilePermission.OWNER_READ)) {
|
||||
ownerPerm = "READ_ONLY";
|
||||
} else if (permissions.contains(PosixFilePermission.OWNER_WRITE)) {
|
||||
ownerPerm = "WRITE_ONLY";
|
||||
}
|
||||
perm.setOwnerAccess(ownerPerm);
|
||||
|
||||
String groupPerm = "NONE";
|
||||
if (permissions.contains(PosixFilePermission.GROUP_READ)
|
||||
&& permissions.contains(PosixFilePermission.GROUP_WRITE)) {
|
||||
groupPerm = "READ_WRITE";
|
||||
} else if (permissions.contains(PosixFilePermission.GROUP_READ)) {
|
||||
groupPerm = "READ_ONLY";
|
||||
} else if (permissions.contains(PosixFilePermission.GROUP_WRITE)) {
|
||||
groupPerm = "WRITE_ONLY";
|
||||
}
|
||||
perm.setGroupAccess(groupPerm);
|
||||
|
||||
String otherPerm = "NONE";
|
||||
if (permissions.contains(PosixFilePermission.OTHERS_READ)
|
||||
&& permissions.contains(PosixFilePermission.OTHERS_WRITE)) {
|
||||
otherPerm = "READ_WRITE";
|
||||
} else if (permissions.contains(PosixFilePermission.OTHERS_READ)) {
|
||||
otherPerm = "READ_ONLY";
|
||||
} else if (permissions.contains(PosixFilePermission.OTHERS_WRITE)) {
|
||||
otherPerm = "WRITE_ONLY";
|
||||
}
|
||||
perm.setOtherAccess(otherPerm);
|
||||
return perm;
|
||||
}
|
||||
|
||||
public void rename(String oldName, String newName, String folder)
|
||||
throws IOException {
|
||||
Files.move(Paths.get(folder, oldName), Paths.get(folder, newName));
|
||||
}
|
||||
|
||||
public void deleteFiles(List<String> files) throws IOException {
|
||||
|
||||
for (String file : files) {
|
||||
System.out.println("Delete: " + file);
|
||||
File f = new File(file);
|
||||
if (f.isDirectory()) {
|
||||
File[] children = f.listFiles();
|
||||
if (children != null) {
|
||||
deleteFiles(Arrays.asList(children).stream()
|
||||
.map(a -> a.getAbsolutePath())
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
if (!f.delete()) {
|
||||
throw new IOException(
|
||||
"Unable to delete: " + f.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the pendingOperations
|
||||
*/
|
||||
public Map<String, FileCopyTask> getPendingOperations() {
|
||||
return pendingOperations;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sourceFile
|
||||
* @param destinationFolder
|
||||
* @param move
|
||||
* @return
|
||||
*/
|
||||
public String createFileCopyTask(List<String> sourceFile,
|
||||
String destinationFolder, boolean move) {
|
||||
FileCopyTask cp = new FileCopyTask(sourceFile, destinationFolder, move);
|
||||
pendingOperations.put(cp.getId(), cp);
|
||||
threadPool.submit(cp);
|
||||
return cp.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
public List<FileCopyProgressResponse> getProgress(List<String> idList) {
|
||||
List<FileCopyProgressResponse> list = new ArrayList<>();
|
||||
for (String id : idList) {
|
||||
FileCopyTask cp = pendingOperations.get(id);
|
||||
FileCopyProgressResponse r = new FileCopyProgressResponse(
|
||||
cp.getId(), cp.getName(), (int) cp.getProgress(),
|
||||
cp.getStatus(), String.join("\n", cp.getErrorMessage()),
|
||||
cp.isHasErrors());
|
||||
list.add(r);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public String getOpName(String id) {
|
||||
return pendingOperations.get(id).getName();
|
||||
}
|
||||
|
||||
public void cancelTask(String id) {
|
||||
FileCopyTask cp = pendingOperations.get(id);
|
||||
cp.cancel();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package webshell.app.files;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.FileAttributeView;
|
||||
import java.nio.file.attribute.FileOwnerAttributeView;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import webshell.app.files.search.SearchResult;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
@Service
|
||||
public class FileService {
|
||||
|
||||
private boolean posix;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public FileService() {
|
||||
posix = !System.getProperty("os.name").toLowerCase()
|
||||
.contains("windows");
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private SearchOperations searchOp;
|
||||
|
||||
@Autowired
|
||||
private FileTypeDetector typeDetector;
|
||||
|
||||
public String createSearch(String folder, String searchText) {
|
||||
return searchOp.createSearchTask(folder, searchText);
|
||||
}
|
||||
|
||||
public void cancelSearch(String id) {
|
||||
searchOp.cancelSearch(id);
|
||||
}
|
||||
|
||||
public SearchResult getSearchResult(String id, int fileIndex,
|
||||
int folderIndex) {
|
||||
return searchOp.getSearchResult(id, fileIndex, folderIndex);
|
||||
}
|
||||
|
||||
private String getFileType(File f) {
|
||||
return typeDetector.getType(f);
|
||||
}
|
||||
|
||||
public List<FileInfo> list(String path) {
|
||||
System.out.println("Listing file: " + path);
|
||||
File[] files = new File(path).listFiles();
|
||||
List<FileInfo> list = new ArrayList<>();
|
||||
if (files == null) {
|
||||
return list;
|
||||
}
|
||||
for (File f : files) {
|
||||
FileInfo info = new FileInfo(f.getName(), f.getAbsolutePath(), null,
|
||||
null, f.isDirectory() ? "Directory" : getFileType(f),
|
||||
f.length(), -1, new Date(f.lastModified()), posix);
|
||||
Set<PosixFilePermission> filePerm = null;
|
||||
try {
|
||||
filePerm = Files.getPosixFilePermissions(f.toPath());
|
||||
String permission = PosixFilePermissions.toString(filePerm);
|
||||
info.setPermissionString(permission);
|
||||
|
||||
FileOwnerAttributeView fv = Files.getFileAttributeView(
|
||||
f.toPath(), FileOwnerAttributeView.class);
|
||||
info.setUser(fv.getOwner().getName());
|
||||
} catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
info.setPermissionString("---");
|
||||
}
|
||||
|
||||
list.add(info);
|
||||
}
|
||||
list.sort((FileInfo a, FileInfo b) -> {
|
||||
String type1 = a.getType();
|
||||
String type2 = b.getType();
|
||||
if (type1.equals("Directory") && type2.equals("Directory")) {
|
||||
return 0;
|
||||
} else if (type1.equals("Directory")) {
|
||||
return -1;
|
||||
} else if (type2.equals("Directory")) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
public void setText(String path, String text) throws Exception {
|
||||
Files.writeString(Paths.get(path), text);
|
||||
}
|
||||
|
||||
public String getText(String path) throws Exception {
|
||||
return new String(Files.readAllBytes(Paths.get(path)), "utf-8");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package webshell.app.files;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
public class FileTransfer {
|
||||
|
||||
private final byte[] BUFFER = new byte[8192];
|
||||
private boolean compress;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public FileTransfer(boolean compress) {
|
||||
this.compress = compress;
|
||||
}
|
||||
|
||||
public void transferFile(String relativePath, String folder,
|
||||
InputStream in) {
|
||||
File f = new File(folder, relativePath);
|
||||
File parent=f.getParentFile();
|
||||
parent.mkdirs();
|
||||
System.out.println("Creating folder: "+parent.getAbsolutePath());
|
||||
System.out.println("Creating file: "+f.getAbsolutePath());
|
||||
try (FileOutputStream out = new FileOutputStream(f)) {
|
||||
copyStream(in, out);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void transferFiles(List<String> files, OutputStream out) {
|
||||
if (!compress) {
|
||||
try (InputStream in = new FileInputStream(files.get(0))) {
|
||||
copyStream(in, out);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
try (ZipOutputStream zout = new ZipOutputStream(out)) {
|
||||
for (String file : files) {
|
||||
File f = new File(file);
|
||||
if (f.isDirectory()) {
|
||||
if (!walkTree(f, zout, f.getName())) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
boolean writing = false;
|
||||
try (InputStream in = new FileInputStream(f)) {
|
||||
ZipEntry ze = new ZipEntry(f.getName());
|
||||
ze.setSize(f.length());
|
||||
writing = true;
|
||||
zout.putNextEntry(ze);
|
||||
copyStream(in, zout);
|
||||
zout.closeEntry();
|
||||
writing = false;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (writing) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean walkTree(File folder, ZipOutputStream zout,
|
||||
String relativePath) {
|
||||
System.out.println(
|
||||
"Walking: " + folder.getAbsolutePath() + " - " + relativePath);
|
||||
try {
|
||||
File[] files = folder.listFiles();
|
||||
if (files != null) {
|
||||
for (File f : files) {
|
||||
if (f.isDirectory()) {
|
||||
if (!walkTree(f, zout, combine(relativePath,
|
||||
f.getName(), File.separator))) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
boolean writing = false;
|
||||
try (InputStream in = new FileInputStream(f)) {
|
||||
ZipEntry ze = new ZipEntry(combine(relativePath,
|
||||
f.getName(), File.separator));
|
||||
ze.setSize(f.length());
|
||||
writing = true;
|
||||
zout.putNextEntry(ze);
|
||||
copyStream(in, zout);
|
||||
zout.closeEntry();
|
||||
writing = false;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (writing) {
|
||||
throw new Exception("error reading file: "
|
||||
+ f.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void copyStream(InputStream in, OutputStream out)
|
||||
throws IOException {
|
||||
while (true) {
|
||||
int x = in.read(BUFFER);
|
||||
if (x == -1)
|
||||
break;
|
||||
out.write(BUFFER, 0, x);
|
||||
}
|
||||
}
|
||||
|
||||
public String combine(String path1, String path2, String separator) {
|
||||
if (path2.startsWith(separator)) {
|
||||
path2 = path2.substring(1);
|
||||
}
|
||||
if (!path1.endsWith(separator)) {
|
||||
return path1 + separator + path2;
|
||||
} else {
|
||||
return path1 + path2;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package webshell.app.files;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.tika.Tika;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
public class FileTypeDetector {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private Tika tika;
|
||||
|
||||
public FileTypeDetector() {
|
||||
tika = new Tika();
|
||||
}
|
||||
|
||||
public synchronized String getType(File file) {
|
||||
try {
|
||||
return tika.detect(file);
|
||||
} catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized String getTypeByName(String file) {
|
||||
return tika.detect(file);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package webshell.app.files;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
public class PosixPermission {
|
||||
private String owner, group;
|
||||
private String ownerAccess, groupAccess, otherAccess;
|
||||
private boolean executable;
|
||||
|
||||
/**
|
||||
* @return the owner
|
||||
*/
|
||||
public String getOwner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param owner the owner to set
|
||||
*/
|
||||
public void setOwner(String owner) {
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the group
|
||||
*/
|
||||
public String getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param group the group to set
|
||||
*/
|
||||
public void setGroup(String group) {
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the ownerAccess
|
||||
*/
|
||||
public String getOwnerAccess() {
|
||||
return ownerAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ownerAccess the ownerAccess to set
|
||||
*/
|
||||
public void setOwnerAccess(String ownerAccess) {
|
||||
this.ownerAccess = ownerAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the groupAccess
|
||||
*/
|
||||
public String getGroupAccess() {
|
||||
return groupAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param groupAccess the groupAccess to set
|
||||
*/
|
||||
public void setGroupAccess(String groupAccess) {
|
||||
this.groupAccess = groupAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the otherAccess
|
||||
*/
|
||||
public String getOtherAccess() {
|
||||
return otherAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param otherAccess the otherAccess to set
|
||||
*/
|
||||
public void setOtherAccess(String otherAccess) {
|
||||
this.otherAccess = otherAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the executable
|
||||
*/
|
||||
public boolean isExecutable() {
|
||||
return executable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param executable the executable to set
|
||||
*/
|
||||
public void setExecutable(boolean executable) {
|
||||
this.executable = executable;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package webshell.app.files;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import webshell.app.files.search.SearchResult;
|
||||
import webshell.app.files.search.SearchTask;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
public class SearchOperations {
|
||||
private final Map<String, SearchTask> pendingOperations = new ConcurrentHashMap<>();
|
||||
private final ExecutorService threadPool = Executors.newFixedThreadPool(5);
|
||||
|
||||
public String createSearchTask(String folder, String searchText) {
|
||||
SearchTask task = new SearchTask(folder, searchText);
|
||||
pendingOperations.put(task.getId(), task);
|
||||
threadPool.submit(task);
|
||||
return task.getId();
|
||||
}
|
||||
|
||||
public SearchResult getSearchResult(String id, int fileIndex,
|
||||
int folderIndex) {
|
||||
SearchTask task = pendingOperations.get(id);
|
||||
SearchResult res = new SearchResult(task.isDone(),
|
||||
task.getFiles().stream().skip(fileIndex)
|
||||
.collect(Collectors.toList()),
|
||||
task.getFolders().stream().skip(folderIndex)
|
||||
.collect(Collectors.toList()));
|
||||
return res;
|
||||
}
|
||||
|
||||
public void cancelSearch(String id) {
|
||||
SearchTask task = pendingOperations.get(id);
|
||||
task.stop();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package webshell.app.files.copy;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
public class FileCopyProgressResponse {
|
||||
private int progress;
|
||||
private String status;
|
||||
private String name;
|
||||
private String errors;
|
||||
private boolean hasErrors;
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* @param progress
|
||||
* @param status
|
||||
* @param errors
|
||||
* @param hasErrors
|
||||
*/
|
||||
public FileCopyProgressResponse(String id, String name, int progress,
|
||||
String status, String errors, boolean hasErrors) {
|
||||
super();
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.progress = progress;
|
||||
this.status = status;
|
||||
this.errors = errors;
|
||||
this.hasErrors = hasErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the progress
|
||||
*/
|
||||
public int getProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param progress the progress to set
|
||||
*/
|
||||
public void setProgress(int progress) {
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the status
|
||||
*/
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param status the status to set
|
||||
*/
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the errors
|
||||
*/
|
||||
public String getErrors() {
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param errors the errors to set
|
||||
*/
|
||||
public void setErrors(String errors) {
|
||||
this.errors = errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the hasErrors
|
||||
*/
|
||||
public boolean isHasErrors() {
|
||||
return hasErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param hasErrors the hasErrors to set
|
||||
*/
|
||||
public void setHasErrors(boolean hasErrors) {
|
||||
this.hasErrors = hasErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the id
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id the id to set
|
||||
*/
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name the name to set
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package webshell.app.files.copy;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
public class FileCopyRequest {
|
||||
private List<String> sourceFile;
|
||||
private String targetFolder;
|
||||
|
||||
/**
|
||||
* @return the sourceFile
|
||||
*/
|
||||
public List<String> getSourceFile() {
|
||||
return sourceFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sourceFile the sourceFile to set
|
||||
*/
|
||||
public void setSourceFile(List<String> sourceFile) {
|
||||
this.sourceFile = sourceFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the targetFolder
|
||||
*/
|
||||
public String getTargetFolder() {
|
||||
return targetFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param targetFolder the targetFolder to set
|
||||
*/
|
||||
public void setTargetFolder(String targetFolder) {
|
||||
this.targetFolder = targetFolder;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,283 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package webshell.app.files.copy;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
public class FileCopyTask implements Runnable {
|
||||
private long total, copied;
|
||||
private List<String> src;
|
||||
private String target;
|
||||
private boolean move;
|
||||
private String status;
|
||||
private final byte[] BUFFER = new byte[8192];
|
||||
private List<String> errorMessage = new ArrayList<>();
|
||||
private boolean hasErrors = false;
|
||||
private AtomicBoolean stopRequested = new AtomicBoolean(false);
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* @param src
|
||||
* @param target
|
||||
* @param move
|
||||
*/
|
||||
public FileCopyTask(List<String> src, String target, boolean move) {
|
||||
super();
|
||||
this.id = UUID.randomUUID().toString();
|
||||
this.src = src;
|
||||
this.target = target;
|
||||
this.move = move;
|
||||
this.status = "Waiting...";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
for (String s : src) {
|
||||
File f = new File(s);
|
||||
calculateSize(f, target);
|
||||
if (stopRequested.get()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (String s : src) {
|
||||
File f = new File(s);
|
||||
moveFiles(f, target);
|
||||
if (stopRequested.get()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.status = "Finished";
|
||||
}
|
||||
|
||||
public double getProgress() {
|
||||
if (total < 1) {
|
||||
return 0;
|
||||
}
|
||||
return ((double) copied * 100) / total;
|
||||
}
|
||||
|
||||
private void calculateSize(File f, String targetFolder) {
|
||||
status = "Preparing...";
|
||||
if (stopRequested.get()) {
|
||||
return;
|
||||
}
|
||||
String name = f.getName();
|
||||
File targetFile = new File(targetFolder, name);
|
||||
|
||||
if (f.isDirectory()) {
|
||||
File files[] = null;
|
||||
try {
|
||||
files = f.listFiles();
|
||||
if (stopRequested.get()) {
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (files == null || files.length < 1) {
|
||||
return;
|
||||
}
|
||||
for (File file : files) {
|
||||
calculateSize(file, targetFile.getAbsolutePath());
|
||||
}
|
||||
} else {
|
||||
total += f.length();
|
||||
}
|
||||
}
|
||||
|
||||
private void moveFiles(File f, String targetFolder) {
|
||||
status = move ? "Moving files ..." : "Copying files ...";
|
||||
if (stopRequested.get()) {
|
||||
return;
|
||||
}
|
||||
String name = f.getName();
|
||||
File targetFile = new File(targetFolder, name);
|
||||
|
||||
if (f.isDirectory()) {
|
||||
targetFile.mkdirs();
|
||||
File files[] = null;
|
||||
try {
|
||||
files = f.listFiles();
|
||||
if (stopRequested.get()) {
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (files == null || files.length < 1) {
|
||||
return;
|
||||
}
|
||||
for (File file : files) {
|
||||
moveFiles(file, targetFile.getAbsolutePath());
|
||||
if (stopRequested.get()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
copy(f, targetFile);
|
||||
}
|
||||
|
||||
if (move) {
|
||||
if (!f.delete()) {
|
||||
hasErrors = true;
|
||||
errorMessage.add("Error deleting: " + src + ";");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void copy(File src, File target) {
|
||||
if (stopRequested.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (InputStream in = new FileInputStream(src);
|
||||
OutputStream out = new FileOutputStream(target)) {
|
||||
while (!stopRequested.get()) {
|
||||
int x = in.read(BUFFER);
|
||||
if (x == -1)
|
||||
break;
|
||||
out.write(BUFFER, 0, x);
|
||||
copied += x;
|
||||
Thread.sleep(10);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
hasErrors = true;
|
||||
e.printStackTrace();
|
||||
errorMessage.add("Error copying: " + src + " to " + target + " - "
|
||||
+ e.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the total
|
||||
*/
|
||||
public long getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param total the total to set
|
||||
*/
|
||||
public void setTotal(long total) {
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the copied
|
||||
*/
|
||||
public long getCopied() {
|
||||
return copied;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param copied the copied to set
|
||||
*/
|
||||
public void setCopied(long copied) {
|
||||
this.copied = copied;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the target
|
||||
*/
|
||||
public String getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param target the target to set
|
||||
*/
|
||||
public void setTarget(String target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the status
|
||||
*/
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param status the status to set
|
||||
*/
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the errorMessage
|
||||
*/
|
||||
public List<String> getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param errorMessage the errorMessage to set
|
||||
*/
|
||||
public void setErrorMessage(List<String> errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the hasErrors
|
||||
*/
|
||||
public boolean isHasErrors() {
|
||||
return hasErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param hasErrors the hasErrors to set
|
||||
*/
|
||||
public void setHasErrors(boolean hasErrors) {
|
||||
this.hasErrors = hasErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the stopRequested
|
||||
*/
|
||||
public AtomicBoolean getStopRequested() {
|
||||
return stopRequested;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stopRequested the stopRequested to set
|
||||
*/
|
||||
public void setStopRequested(AtomicBoolean stopRequested) {
|
||||
this.stopRequested = stopRequested;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the id
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return
|
||||
*/
|
||||
public String getName() {
|
||||
return src.size() == 1 ? src.get(0)
|
||||
: src.get(0) + (src.size() - 1) + " files";
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
this.stopRequested.set(true);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package webshell.app.files.search;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
public class SearchResult {
|
||||
/**
|
||||
* @param isDone
|
||||
* @param files
|
||||
* @param folders
|
||||
*/
|
||||
public SearchResult(boolean isDone, List<String> files,
|
||||
List<String> folders) {
|
||||
super();
|
||||
this.isDone = isDone;
|
||||
this.files = files;
|
||||
this.folders = folders;
|
||||
}
|
||||
|
||||
private boolean isDone;
|
||||
private List<String> files = new ArrayList<>(), folders = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* @return the isDone
|
||||
*/
|
||||
public boolean isDone() {
|
||||
return isDone;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isDone the isDone to set
|
||||
*/
|
||||
public void setDone(boolean isDone) {
|
||||
this.isDone = isDone;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the files
|
||||
*/
|
||||
public List<String> getFiles() {
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param files the files to set
|
||||
*/
|
||||
public void setFiles(List<String> files) {
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the folders
|
||||
*/
|
||||
public List<String> getFolders() {
|
||||
return folders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param folders the folders to set
|
||||
*/
|
||||
public void setFolders(List<String> folders) {
|
||||
this.folders = folders;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package webshell.app.files.search;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.FileVisitor;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
public class SearchTask implements Runnable {
|
||||
/**
|
||||
* @param folder
|
||||
* @param searchText
|
||||
*/
|
||||
public SearchTask(String folder, String searchText) {
|
||||
super();
|
||||
this.id = UUID.randomUUID().toString();
|
||||
this.folder = folder;
|
||||
this.searchText = searchText.toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
|
||||
private String id;
|
||||
private AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
private List<String> files = new ArrayList<>(), folders = new ArrayList<>();
|
||||
private String folder;
|
||||
private String searchText;
|
||||
private AtomicBoolean stopRequested = new AtomicBoolean(false);
|
||||
|
||||
/**
|
||||
* @return the isDone
|
||||
*/
|
||||
public boolean isDone() {
|
||||
return isDone.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isDone the isDone to set
|
||||
*/
|
||||
public void setDone(boolean isDone) {
|
||||
this.isDone.set(isDone);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the files
|
||||
*/
|
||||
public List<String> getFiles() {
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param files the files to set
|
||||
*/
|
||||
public void setFiles(List<String> files) {
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the folders
|
||||
*/
|
||||
public List<String> getFolders() {
|
||||
return folders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param folders the folders to set
|
||||
*/
|
||||
public void setFolders(List<String> folders) {
|
||||
this.folders = folders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the stopRequested
|
||||
*/
|
||||
public AtomicBoolean getStopRequested() {
|
||||
return stopRequested;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public void stop() {
|
||||
System.out.println("Stopping...");
|
||||
this.stopRequested.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
System.out.println("Search start");
|
||||
Files.walkFileTree(Paths.get(folder), new FileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir,
|
||||
BasicFileAttributes attrs) throws IOException {
|
||||
//System.out.println("Search: " + dir.toString());
|
||||
if (stopRequested.get()) {
|
||||
return FileVisitResult.TERMINATE;
|
||||
}
|
||||
|
||||
if (isMatch(dir, searchText)) {
|
||||
folders.add(dir.toAbsolutePath().toString());
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file,
|
||||
BasicFileAttributes attrs) throws IOException {
|
||||
//System.out.println("Search: " + file);
|
||||
if (stopRequested.get()) {
|
||||
return FileVisitResult.TERMINATE;
|
||||
}
|
||||
|
||||
if (isMatch(file, searchText)) {
|
||||
files.add(file.toAbsolutePath().toString());
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFileFailed(Path file,
|
||||
IOException exc) throws IOException {
|
||||
//System.out.println("visit failed: " + file);
|
||||
if (stopRequested.get()) {
|
||||
return FileVisitResult.TERMINATE;
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir,
|
||||
IOException exc) throws IOException {
|
||||
//System.out.println("post visit failed: " + dir);
|
||||
if (stopRequested.get()) {
|
||||
return FileVisitResult.TERMINATE;
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
private boolean isMatch(Path path, String text) {
|
||||
String name = path.toString();
|
||||
return name.toLowerCase(Locale.ENGLISH).contains(text);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
System.out.println("Search finished");
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the searchText
|
||||
*/
|
||||
public String getSearchText() {
|
||||
return searchText;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param searchText the searchText to set
|
||||
*/
|
||||
public void setSearchText(String searchText) {
|
||||
this.searchText = searchText.toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the folder
|
||||
*/
|
||||
public String getFolder() {
|
||||
return folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param folder the folder to set
|
||||
*/
|
||||
public void setFolder(String folder) {
|
||||
this.folder = folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the id
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id the id to set
|
||||
*/
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package webshell.app.security;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class CustomAuthEntryPoint extends BasicAuthenticationEntryPoint {
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException authException) throws IOException, ServletException {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
setRealmName("CustomBasicAuth");
|
||||
super.afterPropertiesSet();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package webshell.app.security;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jws;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import webshell.app.Application;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
|
||||
|
||||
/**
|
||||
* @param authenticationManager
|
||||
*/
|
||||
public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
|
||||
super(authenticationManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response, FilterChain filterChain)
|
||||
throws IOException, ServletException {
|
||||
UsernamePasswordAuthenticationToken authentication = getAuthentication(
|
||||
request);
|
||||
if (authentication == null) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private UsernamePasswordAuthenticationToken getAuthentication(
|
||||
HttpServletRequest request) {
|
||||
String token = request.getHeader("Authorization");
|
||||
if (token != null && token.length() > 0 && token.startsWith("Bearer")) {
|
||||
try {
|
||||
Jws<Claims> parsedToken = Jwts.parser()
|
||||
.setSigningKey(Application.SECRET_KEY)
|
||||
.parseClaimsJws(token.replace("Bearer ", ""));
|
||||
|
||||
String username = parsedToken.getBody().getSubject();
|
||||
|
||||
if (username != null && username.length() > 0) {
|
||||
return new UsernamePasswordAuthenticationToken(System.getProperty("app.default-user"),
|
||||
null, Collections.emptyList());
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
exception.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package webshell.app.security;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import webshell.app.Application;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
@RestController
|
||||
@CrossOrigin(allowCredentials = "true")
|
||||
@RequestMapping("/api")
|
||||
public class JwtTokenController {
|
||||
|
||||
@PostMapping("/token")
|
||||
public Map<String, String> generateToken(Principal principal) {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
String token = Jwts.builder().setSubject(principal.getName())
|
||||
.signWith(Application.SECRET_KEY).compact();
|
||||
map.put("token", token);
|
||||
map.put("posix", (!System.getProperty("os.name").toLowerCase()
|
||||
.contains("windows")) + "");
|
||||
return map;
|
||||
}
|
||||
|
||||
@PostMapping("/auth")
|
||||
public void checkToken() {
|
||||
}
|
||||
|
||||
@GetMapping("/token/temp")
|
||||
public Map<String, String> generateTempToken(Principal principal) {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
String token = Jwts.builder().setSubject(principal.getName())
|
||||
.setExpiration(
|
||||
new Date(System.currentTimeMillis() + (60 * 1000)))
|
||||
.signWith(Application.SECRET_KEY).compact();
|
||||
map.put("token", token);
|
||||
return map;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package webshell.app.security;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
public class WebshellUserDetailsService implements UserDetailsService {
|
||||
Properties users = new Properties();
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public WebshellUserDetailsService() {
|
||||
loadProperties();
|
||||
}
|
||||
|
||||
private void loadProperties() {
|
||||
users.clear();
|
||||
String propertyPath = System.getenv("easy-web-shell.config-path");
|
||||
if (propertyPath == null) {
|
||||
propertyPath = System.getProperty("user.home");
|
||||
}
|
||||
|
||||
File f = new File(propertyPath, ".users");
|
||||
|
||||
if (f.exists()) {
|
||||
try (InputStream in = new FileInputStream(f)) {
|
||||
users.load(in);
|
||||
} catch (FileNotFoundException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (users.size() < 1) {
|
||||
users.put("admin", "admin");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username)
|
||||
throws UsernameNotFoundException {
|
||||
if (!users.containsKey(username)) {
|
||||
return null;
|
||||
}
|
||||
String password = users.getProperty(username);
|
||||
List<GrantedAuthority> authorities = new ArrayList<>();
|
||||
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
|
||||
return new User(username, password, authorities);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,247 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package webshell.app.terminal;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import com.pty4j.PtyProcess;
|
||||
import com.pty4j.PtyProcessBuilder;
|
||||
import com.pty4j.WinSize;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
public class PtySession implements Runnable {
|
||||
private String id;
|
||||
private WebSocketSession ws;
|
||||
private Thread t;
|
||||
private PtyProcess pty;
|
||||
private OutputStream out;
|
||||
private InputStream os;
|
||||
private boolean ptyAllowed;
|
||||
private SshPtyProcess proc;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public PtySession() throws Exception {
|
||||
id = UUID.randomUUID().toString();
|
||||
|
||||
ptyAllowed = true;
|
||||
String[] cmd = "auto".equals(System.getProperty("app.default-shell"))
|
||||
? System.getProperty("os.name").toLowerCase()
|
||||
.contains("windows") ? new String[] { "cmd" }
|
||||
: new String[] { "/bin/bash", "-l" }
|
||||
: new String[] { System.getProperty("app.default-shell") };
|
||||
|
||||
try {
|
||||
// The initial environment to pass to the PTY child process...
|
||||
Map<String, String> env = new HashMap<>();
|
||||
env.put("TERM", "xterm");
|
||||
PtyProcessBuilder pb = new PtyProcessBuilder(cmd);
|
||||
pb.setRedirectErrorStream(true);
|
||||
pb.setEnvironment(env);
|
||||
pty = pb.start();
|
||||
os = pty.getInputStream();
|
||||
out = pty.getOutputStream();
|
||||
System.out.println("Pty created");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
if ("true".equals(System.getProperty("app.fallback-local-ssh"))) {
|
||||
proc = new SshPtyProcess(
|
||||
System.getProperty("app.local-ssh-host"),
|
||||
System.getProperty("app.local-ssh-user"),
|
||||
Integer.parseInt(
|
||||
System.getProperty("app.local-ssh-port")),
|
||||
System.getProperty("app.local-ssh-password"),
|
||||
System.getProperty("app.local-ssh-keyfile"),
|
||||
System.getProperty("app.local-ssh-passphrase"));
|
||||
proc.connect();
|
||||
os = proc.getIn();
|
||||
out = proc.getOut();
|
||||
}
|
||||
}
|
||||
|
||||
// if (System.getProperty("os.name").toLowerCase().contains("windows")) {
|
||||
// ptyAllowed = false;
|
||||
// ProcessBuilder pb = new ProcessBuilder("powershell");
|
||||
// pb.redirectErrorStream(true);
|
||||
// proc = pb.start();
|
||||
// os = proc.getInputStream();
|
||||
// out = proc.getOutputStream();
|
||||
// System.out.println("Process created");
|
||||
// windows = true;
|
||||
// } else {
|
||||
// ptyAllowed = true;
|
||||
// String[] cmd = { "/bin/bash", "-l" };
|
||||
// // The initial environment to pass to the PTY child process...
|
||||
// Map<String, String> env = new HashMap<>();
|
||||
// env.put("TERM", "xterm");
|
||||
// PtyProcessBuilder pb = new PtyProcessBuilder(cmd);
|
||||
// pb.setRedirectErrorStream(true);
|
||||
// pb.setEnvironment(env);
|
||||
// pty = pb.start();
|
||||
// os = pty.getInputStream();
|
||||
// out = pty.getOutputStream();
|
||||
// System.out.println("Pty created");
|
||||
// windows = false;
|
||||
// }
|
||||
}
|
||||
|
||||
public void start() {
|
||||
t = new Thread(this);
|
||||
t.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
byte[] buf = new byte[512];
|
||||
try {
|
||||
while (true) {
|
||||
int x = os.read(buf);
|
||||
if (x == -1) {
|
||||
System.out.println("Stream end");
|
||||
break;
|
||||
}
|
||||
ws.sendMessage(
|
||||
new TextMessage(new String(buf, 0, x, "utf-8")));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
if (ptyAllowed && pty != null) {
|
||||
System.out.println("Waiting for pty...");
|
||||
pty.waitFor();
|
||||
} else if (proc != null) {
|
||||
System.out.println("Waiting fot proc...");
|
||||
proc.waitFor();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (pty != null) {
|
||||
pty.destroy();
|
||||
}
|
||||
if (proc != null) {
|
||||
proc.close();
|
||||
}
|
||||
}
|
||||
System.out.println("Thread finished");
|
||||
}
|
||||
|
||||
public void sendData(String text) {
|
||||
try {
|
||||
// if (windows) {
|
||||
// ws.sendMessage(new TextMessage(text));
|
||||
// if (text.equals("\r")) {
|
||||
// System.out.println("replaced");
|
||||
// text = "\r\n";
|
||||
// }
|
||||
// out.write(text.getBytes("utf-8"));
|
||||
// out.flush();
|
||||
// } else {
|
||||
// out.write(text.getBytes("utf-8"));
|
||||
// out.flush();
|
||||
// }
|
||||
|
||||
// if (text.equals("\n")) {
|
||||
// out.write("\r\n".getBytes());
|
||||
// } else {
|
||||
// System.out.println("sending to text: " + text + " len: "
|
||||
// + text.length() + " char " + (int) text.charAt(0)
|
||||
// + " - " + (int) '\n');
|
||||
// out.write(text.getBytes());// text.getBytes("utf-8"));
|
||||
// out.flush();
|
||||
// }
|
||||
|
||||
out.write(text.getBytes("utf-8"));
|
||||
out.flush();
|
||||
|
||||
System.out.println("sent text");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (pty != null) {
|
||||
pty.destroyForcibly();
|
||||
}
|
||||
if (proc != null) {
|
||||
proc.close();
|
||||
}
|
||||
}
|
||||
|
||||
public void resizePty(int row, int col) {
|
||||
if (pty != null) {
|
||||
pty.setWinSize(new WinSize(col, row));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the id
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id the id to set
|
||||
*/
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the ws
|
||||
*/
|
||||
public WebSocketSession getWs() {
|
||||
return ws;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ws the ws to set
|
||||
*/
|
||||
public void setWs(WebSocketSession ws) {
|
||||
this.ws = ws;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the t
|
||||
*/
|
||||
public Thread getT() {
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param t the t to set
|
||||
*/
|
||||
public void setT(Thread t) {
|
||||
this.t = t;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the pty
|
||||
*/
|
||||
public PtyProcess getPty() {
|
||||
return pty;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pty the pty to set
|
||||
*/
|
||||
public void setPty(PtyProcess pty) {
|
||||
this.pty = pty;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,302 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package webshell.app.terminal;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import com.jcraft.jsch.ChannelShell;
|
||||
import com.jcraft.jsch.JSch;
|
||||
import com.jcraft.jsch.Session;
|
||||
import com.jcraft.jsch.UserInfo;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
public class SshPtyProcess {
|
||||
|
||||
private JSch jsch;
|
||||
private Session session;
|
||||
private String host;
|
||||
private String user;
|
||||
private int port;
|
||||
private String keyFile, passphrase;
|
||||
private String password;
|
||||
|
||||
private OutputStream out;
|
||||
private ChannelShell shell;
|
||||
private InputStream in;
|
||||
|
||||
/**
|
||||
* @param host
|
||||
* @param user
|
||||
* @param port
|
||||
* @param password
|
||||
* @param keyFile
|
||||
* @param passphrase
|
||||
*/
|
||||
public SshPtyProcess(String host, String user, int port, String password,
|
||||
String keyFile, String passphrase) {
|
||||
super();
|
||||
this.host = host;
|
||||
this.user = user;
|
||||
this.port = port;
|
||||
this.password = password;
|
||||
this.keyFile = keyFile;
|
||||
this.passphrase = passphrase;
|
||||
}
|
||||
|
||||
public void connect() throws Exception {
|
||||
MyUserInfo info = new MyUserInfo();
|
||||
jsch = new JSch();
|
||||
JSch.setConfig("MaxAuthTries", "5");
|
||||
|
||||
if (keyFile != null && keyFile.length() > 0) {
|
||||
jsch.addIdentity(keyFile);
|
||||
}
|
||||
|
||||
session = jsch.getSession(user, host, port);
|
||||
|
||||
session.setUserInfo(info);
|
||||
|
||||
session.setPassword(info.getPassword());
|
||||
// session.setConfig("StrictHostKeyChecking", "no");
|
||||
session.setConfig("PreferredAuthentications",
|
||||
"publickey,keyboard-interactive,password");
|
||||
|
||||
session.connect();
|
||||
|
||||
System.out.println("Client version: " + session.getClientVersion());
|
||||
System.out.println("Server host: " + session.getHost());
|
||||
System.out.println("Server version: " + session.getServerVersion());
|
||||
System.out.println(
|
||||
"Hostkey: " + session.getHostKey().getFingerPrint(jsch));
|
||||
|
||||
shell = (ChannelShell) session.openChannel("shell");
|
||||
in = shell.getInputStream();
|
||||
out = shell.getOutputStream();
|
||||
shell.connect();
|
||||
}
|
||||
|
||||
class MyUserInfo implements UserInfo {
|
||||
|
||||
@Override
|
||||
public String getPassphrase() {
|
||||
return passphrase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean promptPassword(String message) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean promptPassphrase(String message) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean promptYesNo(String message) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showMessage(String message) {
|
||||
try {
|
||||
out.write(message.getBytes());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the jsch
|
||||
*/
|
||||
public JSch getJsch() {
|
||||
return jsch;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param jsch the jsch to set
|
||||
*/
|
||||
public void setJsch(JSch jsch) {
|
||||
this.jsch = jsch;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the session
|
||||
*/
|
||||
public Session getSession() {
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param session the session to set
|
||||
*/
|
||||
public void setSession(Session session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the host
|
||||
*/
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param host the host to set
|
||||
*/
|
||||
public void setHost(String host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the user
|
||||
*/
|
||||
public String getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param user the user to set
|
||||
*/
|
||||
public void setUser(String user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the port
|
||||
*/
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param port the port to set
|
||||
*/
|
||||
public void setPort(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the keyFile
|
||||
*/
|
||||
public String getKeyFile() {
|
||||
return keyFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param keyFile the keyFile to set
|
||||
*/
|
||||
public void setKeyFile(String keyFile) {
|
||||
this.keyFile = keyFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the passphrase
|
||||
*/
|
||||
public String getPassphrase() {
|
||||
return passphrase;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param passphrase the passphrase to set
|
||||
*/
|
||||
public void setPassphrase(String passphrase) {
|
||||
this.passphrase = passphrase;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the password
|
||||
*/
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param password the password to set
|
||||
*/
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the out
|
||||
*/
|
||||
public OutputStream getOut() {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param out the out to set
|
||||
*/
|
||||
public void setOut(OutputStream out) {
|
||||
this.out = out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the shell
|
||||
*/
|
||||
public ChannelShell getShell() {
|
||||
return shell;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param shell the shell to set
|
||||
*/
|
||||
public void setShell(ChannelShell shell) {
|
||||
this.shell = shell;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the in
|
||||
*/
|
||||
public InputStream getIn() {
|
||||
return in;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param in the in to set
|
||||
*/
|
||||
public void setIn(InputStream in) {
|
||||
this.in = in;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
shell.disconnect();
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
try {
|
||||
session.disconnect();
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
|
||||
System.out.println("Ssh shell closed");
|
||||
}
|
||||
|
||||
public void waitFor() {
|
||||
while (shell.isConnected()) {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package webshell.app.terminal;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import webshell.app.AppContext;
|
||||
import webshell.app.Application;
|
||||
|
||||
/**
|
||||
* @author subhro
|
||||
*
|
||||
*/
|
||||
public class TerminalWebsocketHandler extends AbstractWebSocketHandler {
|
||||
private static final Logger logger = LoggerFactory
|
||||
.getLogger(TerminalWebsocketHandler.class);
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session)
|
||||
throws Exception {
|
||||
logger.info("Incoming session: " + session.getId() + " url: "
|
||||
+ session.getUri());
|
||||
|
||||
synchronized (this) {
|
||||
URI uri = session.getUri();
|
||||
MultiValueMap<String, String> params = UriComponentsBuilder
|
||||
.fromUri(uri).build().getQueryParams();
|
||||
|
||||
try {
|
||||
String token = params.getFirst("token");
|
||||
Jwts.parser().setSigningKey(Application.SECRET_KEY)
|
||||
.parseClaimsJws(token);
|
||||
} catch (Exception exception) {
|
||||
exception.printStackTrace();
|
||||
session.close(CloseStatus.BAD_DATA);
|
||||
return;
|
||||
}
|
||||
|
||||
String appId = params.getFirst("id");
|
||||
PtySession app = AppContext.INSTANCES.get(appId);
|
||||
if (app == null) {
|
||||
logger.error("No instance for session id: " + session.getId()
|
||||
+ " data: " + uri);
|
||||
session.close(CloseStatus.BAD_DATA);
|
||||
return;
|
||||
}
|
||||
app.setWs(session);
|
||||
AppContext.SESSION_MAP.put(session.getId(), app);
|
||||
app.start();
|
||||
}
|
||||
|
||||
logger.info("Handshake complete session: " + session.getId() + " url: "
|
||||
+ session.getUri());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleTextMessage(WebSocketSession session,
|
||||
TextMessage message) throws Exception {
|
||||
System.out.println("Message received: " + message.getPayload());
|
||||
|
||||
PtySession app = AppContext.SESSION_MAP.get(session.getId());
|
||||
|
||||
if (app == null) {
|
||||
logger.error("Invalid app id: " + session.getId());
|
||||
session.close(CloseStatus.BAD_DATA);
|
||||
return;
|
||||
}
|
||||
|
||||
app.sendData(message.getPayload());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session,
|
||||
CloseStatus status) throws Exception {
|
||||
System.out.println("Session closed: " + session.getId());
|
||||
PtySession app = AppContext.SESSION_MAP.get(session.getId());
|
||||
if (app != null) {
|
||||
app.close();
|
||||
AppContext.SESSION_MAP.remove(session.getId());
|
||||
logger.info("Session closed");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
server.port=8055
|
||||
app.default-admin-user=admin
|
||||
app.default-admin-pass=$2a$10$U8V0ssyrquzGKBjGu3ui5esNr9mdGoyeOfPC9K7SFxX2ISA9T8VlC
|
||||
# The format used for the keystore. It could be set to JKS in case it is a JKS file
|
||||
server.ssl.key-store-type=PKCS12
|
||||
# The path to the keystore containing the certificate
|
||||
#server.ssl.key-store=classpath:keystore/easy-webshell.p12
|
||||
# The password used to generate the certificate
|
||||
#server.ssl.key-store-password=webshell
|
||||
# The alias mapped to the certificate
|
||||
#server.ssl.key-alias=easy-webshell
|
||||
|
||||
security.require-ssl=true
|
|
@ -0,0 +1,405 @@
|
|||
xterm
|
||||
MIT
|
||||
Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||
Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
|
||||
Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
webpack
|
||||
MIT
|
||||
Copyright JS Foundation and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
ace-builds
|
||||
BSD-3-Clause
|
||||
Copyright (c) 2010, Ajax.org B.V.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of Ajax.org B.V. nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
tslib
|
||||
Apache-2.0
|
||||
Apache License
|
||||
|
||||
Version 2.0, January 2004
|
||||
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||
|
||||
You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||
|
||||
You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||
|
||||
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||
|
||||
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
|
||||
rxjs
|
||||
Apache-2.0
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
|
||||
|
||||
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
|
||||
|
||||
http://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.
|
||||
|
||||
|
||||
|
||||
@angular/core
|
||||
MIT
|
||||
|
||||
@angular/common
|
||||
MIT
|
||||
|
||||
@angular/platform-browser
|
||||
MIT
|
||||
|
||||
@angular/router
|
||||
MIT
|
||||
|
||||
@angular/forms
|
||||
MIT
|
||||
|
||||
@ng-bootstrap/ng-bootstrap
|
||||
MIT
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2018 Angular ng-bootstrap team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
zone.js
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2016-2018 Google, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"path": "/home/subhro",
|
||||
"files": [
|
||||
{
|
||||
"name": "abc",
|
||||
"type": "File",
|
||||
"size": 1000,
|
||||
"lastModified": "12-01-2019",
|
||||
"permission": "rwxrwxrwx",
|
||||
"permissionString": "rwxrwxrwx",
|
||||
"user": "subhro",
|
||||
"path": "/home/subhro/abc"
|
||||
},
|
||||
{
|
||||
"name": "xyz",
|
||||
"type": "Directory",
|
||||
"size": 0,
|
||||
"lastModified": "12-01-2019",
|
||||
"permission": "rwxrwxrwx",
|
||||
"permissionString": "rwxrwxrwx",
|
||||
"user": "subhro",
|
||||
"path": "/home/subhro/xyz"
|
||||
}
|
||||
]
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 80 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 434 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>WebShellJs</title>
|
||||
<base href="/">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="stylesheet" href="styles.b6dc42ec3e3144ef7548.css"></head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
<script type="text/javascript" src="runtime.26209474bfa8dc87a77c.js"></script><script type="text/javascript" src="polyfills.bebee6a5ef0ece001bc6.js"></script><script type="text/javascript" src="main.acd0e4f8467e6ce3d944.js"></script></body>
|
||||
</html>
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
|||
!function(e){function r(r){for(var n,f,i=r[0],l=r[1],a=r[2],c=0,s=[];c<i.length;c++)o[f=i[c]]&&s.push(o[f][0]),o[f]=0;for(n in l)Object.prototype.hasOwnProperty.call(l,n)&&(e[n]=l[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,a||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++)0!==o[t[i]]&&(n=!1);n&&(u.splice(r--,1),e=f(f.s=t[0]))}return e}var n={},o={0:0},u=[];function f(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.m=e,f.c=n,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(e,r){if(1&r&&(e=f(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)f.d(t,n,(function(r){return e[r]}).bind(null,n));return t},f.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return f.d(r,"a",r),r},f.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},f.p="";var i=window.webpackJsonp=window.webpackJsonp||[],l=i.push.bind(i);i.push=r,i=i.slice();for(var a=0;a<i.length;a++)r(i[a]);var p=l;t()}([]);
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,24 @@
|
|||
package webshell.app;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest
|
||||
public class AppApplicationTests {
|
||||
|
||||
@Autowired
|
||||
private BCryptPasswordEncoder passwordEncoder;
|
||||
|
||||
@Test
|
||||
public void contextLoads() {
|
||||
System.out.println("Password match: " + passwordEncoder.matches("Starscream@64",
|
||||
"$2a$10$greBvSdJwMfmrz7Fof0mB.i2oiBNypVeGa9KCBOZ2BPMxXBa3xJUK"));
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
|
@ -0,0 +1,44 @@
|
|||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# profiling files
|
||||
chrome-profiler-events.json
|
||||
speed-measure-plugin.json
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
|
@ -0,0 +1,27 @@
|
|||
# WebShellJs
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.2.2.
|
||||
|
||||
## Development server
|
||||
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
|
@ -0,0 +1,135 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"web-shell-js": {
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"projectType": "application",
|
||||
"prefix": "app",
|
||||
"schematics": {},
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/web-shell-js",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "web-shell-js:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "web-shell-js:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "web-shell-js:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"karmaConfig": "src/karma.conf.js",
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": [],
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"src/tsconfig.app.json",
|
||||
"src/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"web-shell-js-e2e": {
|
||||
"root": "e2e/",
|
||||
"projectType": "application",
|
||||
"prefix": "",
|
||||
"architect": {
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "web-shell-js:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "web-shell-js:serve:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": "e2e/tsconfig.e2e.json",
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "web-shell-js"
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
'browserName': 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.e2e.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
import { AppPage } from './app.po';
|
||||
import { browser, logging } from 'protractor';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getTitleText()).toEqual('Welcome to web-shell-js!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
expect(logs).not.toContain(jasmine.objectContaining({
|
||||
level: logging.Level.SEVERE,
|
||||
}));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo() {
|
||||
return browser.get('/');
|
||||
}
|
||||
|
||||
getTitleText() {
|
||||
return element(by.css('app-root h1')).getText();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/app",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"name": "web-shell-js",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~7.2.0",
|
||||
"@angular/common": "~7.2.0",
|
||||
"@angular/compiler": "~7.2.0",
|
||||
"@angular/core": "~7.2.0",
|
||||
"@angular/forms": "~7.2.0",
|
||||
"@angular/platform-browser": "~7.2.0",
|
||||
"@angular/platform-browser-dynamic": "~7.2.0",
|
||||
"@angular/router": "~7.2.0",
|
||||
"@ng-bootstrap/ng-bootstrap": "^4.2.0",
|
||||
"bootstrap": "^4.3.1",
|
||||
"core-js": "^2.5.4",
|
||||
"font-awesome": "^4.7.0",
|
||||
"ng2-ace-editor": "^0.3.9",
|
||||
"rxjs": "~6.3.3",
|
||||
"tslib": "^1.9.0",
|
||||
"xterm": "^3.13.2",
|
||||
"zone.js": "~0.8.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.12.0",
|
||||
"@angular/cli": "~7.2.2",
|
||||
"@angular/compiler-cli": "~7.2.0",
|
||||
"@angular/language-service": "~7.2.0",
|
||||
"@types/node": "~8.9.4",
|
||||
"@types/jasmine": "~2.8.8",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"codelyzer": "~4.5.0",
|
||||
"jasmine-core": "~2.99.1",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "~3.1.1",
|
||||
"karma-chrome-launcher": "~2.2.0",
|
||||
"karma-coverage-istanbul-reporter": "~2.0.1",
|
||||
"karma-jasmine": "~1.1.2",
|
||||
"karma-jasmine-html-reporter": "^0.2.2",
|
||||
"protractor": "~5.4.0",
|
||||
"ts-node": "~7.0.0",
|
||||
"tslint": "~5.11.0",
|
||||
"typescript": "~3.2.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { HomeComponent } from './home/home.component';
|
||||
import { FilesComponent } from './home/files/files.component';
|
||||
import { SearchComponent } from './home/search/search.component';
|
||||
import { MonitoringComponent } from './home/monitoring/monitoring.component';
|
||||
import { EditorComponent } from './home/editor/editor.component';
|
||||
import { LoginComponent } from './login/login.component';
|
||||
import { AuthGuardGuard } from './guards/auth-guard.guard';
|
||||
import { SettingsComponent } from './home/settings/settings.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'app',
|
||||
component: HomeComponent,
|
||||
canActivate: [AuthGuardGuard],
|
||||
// children: [
|
||||
// {
|
||||
// path: 'files',
|
||||
// component: FilesComponent
|
||||
// },
|
||||
// {
|
||||
// path: 'search',
|
||||
// component: SearchComponent
|
||||
// },
|
||||
// {
|
||||
// path: 'monitoring',
|
||||
// component: MonitoringComponent
|
||||
// },
|
||||
// {
|
||||
// path: 'editor',
|
||||
// component: EditorComponent
|
||||
// },
|
||||
// {
|
||||
// path: 'settings',
|
||||
// component: SettingsComponent
|
||||
// }
|
||||
// ]
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
component: LoginComponent,
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'app',
|
||||
pathMatch: 'full'
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
|
@ -0,0 +1 @@
|
|||
<router-outlet></router-outlet>
|
|
@ -0,0 +1,35 @@
|
|||
import { TestBed, async } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule
|
||||
],
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have as title 'web-shell-js'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app.title).toEqual('web-shell-js');
|
||||
});
|
||||
|
||||
it('should render title in a h1 tag', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.debugElement.nativeElement;
|
||||
expect(compiled.querySelector('h1').textContent).toContain('Welcome to web-shell-js!');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'web-shell-js';
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { HomeComponent } from './home/home.component';
|
||||
import { FilesComponent } from './home/files/files.component';
|
||||
import { SearchComponent } from './home/search/search.component';
|
||||
import { MonitoringComponent } from './home/monitoring/monitoring.component';
|
||||
import { BrowserComponent } from './home/files/browser/browser.component';
|
||||
import { UploaderProgressComponent } from './home/uploader-progress/uploader-progress.component';
|
||||
import { TerminalComponent } from './home/terminal/terminal.component';
|
||||
import { TreeComponent } from './home/files/tree/tree.component';
|
||||
import { EditorComponent } from './home/editor/editor.component';
|
||||
import { RenameComponent } from './home/files/browser/rename/rename.component';
|
||||
import { InfoComponent } from './home/files/browser/info/info.component';
|
||||
import { LoginComponent } from './login/login.component';
|
||||
import { httpInterceptorProviders } from './intercepters/auth';
|
||||
import { ImageViewerComponent } from './home/files/viewer/image-viewer/image-viewer.component';
|
||||
import { MediaPlayerComponent } from './home/files/viewer/media-player/media-player.component';
|
||||
import { SettingsComponent } from './home/settings/settings.component';
|
||||
import { UnsupportedContentViewerComponent } from './home/files/viewer/unsupported-content-viewer/unsupported-content-viewer.component';
|
||||
import { NewItemComponent } from './home/files/browser/new-item/new-item.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
HomeComponent,
|
||||
FilesComponent,
|
||||
SearchComponent,
|
||||
MonitoringComponent,
|
||||
TerminalComponent,
|
||||
BrowserComponent,
|
||||
UploaderProgressComponent,
|
||||
TreeComponent,
|
||||
EditorComponent,
|
||||
RenameComponent,
|
||||
InfoComponent,
|
||||
LoginComponent,
|
||||
ImageViewerComponent,
|
||||
MediaPlayerComponent,
|
||||
SettingsComponent,
|
||||
UnsupportedContentViewerComponent,
|
||||
NewItemComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
AppRoutingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbModule,
|
||||
HttpClientModule
|
||||
],
|
||||
providers: [httpInterceptorProviders],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
|
@ -0,0 +1,12 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DataService } from './data.service';
|
||||
|
||||
describe('DataService', () => {
|
||||
beforeEach(() => TestBed.configureTestingModule({}));
|
||||
|
||||
it('should be created', () => {
|
||||
const service: DataService = TestBed.get(DataService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,400 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpRequest, HttpEventType } from '@angular/common/http';
|
||||
import { environment } from '../environments/environment';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { FileUploadItem } from './model/file-upload-item';
|
||||
import { FolderUploadItem } from './model/folder-upload-item';
|
||||
import { TerminalSession } from './model/terminal-session';
|
||||
import { FolderTab } from './model/folder-tab';
|
||||
import { NavigationTreeNode } from './model/navigation-tree-node';
|
||||
import { EditorContext } from './model/editor-context';
|
||||
import { FileOperationItem } from './model/file-operation-item';
|
||||
import { SearchContext } from './model/search-context';
|
||||
import { PosixPermissions } from './model/posix-permissions';
|
||||
|
||||
|
||||
const MAX_ACTIVE_UPLOAD = 5;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DataService {
|
||||
|
||||
jwtToken: string;
|
||||
|
||||
searchContext: SearchContext = new SearchContext();
|
||||
|
||||
editorContexts = {};
|
||||
|
||||
selectedEditorTab: string;
|
||||
|
||||
copiedFilePath = {
|
||||
files: [],
|
||||
cut: false
|
||||
};
|
||||
|
||||
sharedMenuListener = new Subject();
|
||||
newTabListener = new Subject<FolderTab>();
|
||||
currentTabListener = new Subject<any>();
|
||||
|
||||
tree1: NavigationTreeNode = {
|
||||
path: null,
|
||||
name: "Bookmarks",
|
||||
expanded: false,
|
||||
children: null,
|
||||
leafNode: false
|
||||
};
|
||||
|
||||
tree2: NavigationTreeNode = {
|
||||
path: null,
|
||||
name: "Home",
|
||||
expanded: true,
|
||||
children: null,
|
||||
leafNode: false
|
||||
};
|
||||
|
||||
tree3: NavigationTreeNode = {
|
||||
path: null,
|
||||
name: "File system",
|
||||
expanded: true,
|
||||
children: null,
|
||||
leafNode: false
|
||||
};
|
||||
|
||||
posix:boolean;
|
||||
|
||||
tabs: FolderTab[] = [];
|
||||
selectedTab: number;
|
||||
|
||||
uploads: any[] = [];
|
||||
activeUploads: number = 0;
|
||||
|
||||
fileOperations: FileOperationItem[] = [];
|
||||
|
||||
fileOpMonitor: any;
|
||||
|
||||
terminalSession: TerminalSession;
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
this.fileOpMonitor = setInterval(() => {
|
||||
if (this.fileOperations && this.fileOperations.length > 0) {
|
||||
this.updateFileOpProgress();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
public connect(): Observable<any> {
|
||||
return this.http.post<any>(environment.BASE_URL + "app/terminal", {});
|
||||
}
|
||||
|
||||
public listFiles(path: string): Observable<any> {
|
||||
return this.http.get<any>(environment.BASE_URL + "app/files/list/" + btoa(path));
|
||||
}
|
||||
|
||||
public goUp(path: string): Observable<any> {
|
||||
return this.http.get<any>(environment.BASE_URL + "app/files/up/" + btoa(path));
|
||||
}
|
||||
|
||||
public listHome(): Observable<any> {
|
||||
return this.http.get<any>(environment.BASE_URL + "app/files/home");
|
||||
}
|
||||
|
||||
public downloadFiles(folder: string, files: string, tempToken:string) {
|
||||
window.location.href = environment.BIN_URL + "download?folder=" + btoa(folder) + "&files=" + btoa(files) + "&token=" + tempToken;
|
||||
}
|
||||
|
||||
public uploadNext() {
|
||||
if (this.uploads.length > 0 && this.activeUploads < MAX_ACTIVE_UPLOAD) {
|
||||
for (let i = 0; i < this.uploads.length; i++) {
|
||||
if (!this.uploads[i].status) {
|
||||
if (this.uploads[i] instanceof FileUploadItem) {
|
||||
this.uploadFile(this.uploads[i]);
|
||||
}
|
||||
else {
|
||||
this.uploadFolderPart(this.uploads[i]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public uploadFile(item: FileUploadItem) {
|
||||
item.status = "Inprogress";
|
||||
console.log("Starting upload of " + item.name);
|
||||
let url = environment.BASE_URL + "app/upload/" + btoa(item.folder) + "/" + btoa(item.relativePath);
|
||||
this.activeUploads++;
|
||||
let req = new HttpRequest<any>("POST", url, item.file, {
|
||||
reportProgress: true
|
||||
});
|
||||
item.subscription = this.http.request(req).subscribe((event: any) => {
|
||||
if (event.type === HttpEventType.UploadProgress) {
|
||||
item.bytesUploaded += event.loaded;
|
||||
item.percent = Math.floor((item.bytesUploaded * 100) / item.size);
|
||||
}
|
||||
else if (event.type === HttpEventType.Response) {
|
||||
for (let i = 0; i < this.uploads.length; i++) {
|
||||
if (this.uploads[i] == item) {
|
||||
this.uploads.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.activeUploads--;
|
||||
this.uploadNext();
|
||||
}
|
||||
}, (error => {
|
||||
item.status = 'Failed';
|
||||
console.log("Error");
|
||||
this.activeUploads--;
|
||||
}));
|
||||
}
|
||||
|
||||
public uploadItem(file: File, folder: string, relativePath: string): void {
|
||||
console.log("adding for upload: " + file.name)
|
||||
let item: FileUploadItem = new FileUploadItem();
|
||||
item.relativePath = relativePath;
|
||||
item.folder = folder;
|
||||
item.file = file;
|
||||
item.size = file.size;
|
||||
item.name = file.name;
|
||||
this.uploads.push(item);
|
||||
this.uploadNext();
|
||||
}
|
||||
|
||||
public cancelUpload(item: any) {
|
||||
console.log("Cancelling: " + item.name);
|
||||
item.subscription.unsubscribe();
|
||||
for (let i = 0; i < this.uploads.length; i++) {
|
||||
if (this.uploads[i] == item) {
|
||||
this.uploads.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.uploadNext();
|
||||
}
|
||||
|
||||
public uploadFolderPart(item: FolderUploadItem) {
|
||||
item.status = "Inprogress";
|
||||
console.log("Starting upload of " + item.name);
|
||||
this.activeUploads++;
|
||||
let url = environment.BASE_URL +
|
||||
"app/upload/" +
|
||||
btoa(item.basePath) + "/" + btoa(item.relatvePaths[item.needle]);
|
||||
|
||||
let req = new HttpRequest<any>("POST", url, item.files[item.needle], {
|
||||
reportProgress: true
|
||||
});
|
||||
|
||||
item.subscription = this.http.request(req).subscribe((event: any) => {
|
||||
if (event.type === HttpEventType.UploadProgress) {
|
||||
item.bytesUploaded += event.loaded;
|
||||
item.percent = Math.floor((item.bytesUploaded * 100) / item.size);
|
||||
}
|
||||
else if (event.type === HttpEventType.Response) {
|
||||
this.activeUploads--;
|
||||
item.needle++;
|
||||
if (item.needle < item.files.length) {
|
||||
this.uploadFolderPart(item);
|
||||
} else {
|
||||
for (let i = 0; i < this.uploads.length; i++) {
|
||||
if (this.uploads[i] == item) {
|
||||
this.uploads.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.uploadNext();
|
||||
}
|
||||
}
|
||||
}, (error => {
|
||||
item.status = 'Failed';
|
||||
console.log("Error");
|
||||
this.activeUploads--;
|
||||
}));
|
||||
}
|
||||
|
||||
public uploadFolder(relatvePaths: string[], files: File[], basePath: string, name: string) {
|
||||
let item = new FolderUploadItem();
|
||||
item.name = name;
|
||||
item.basePath = basePath;
|
||||
item.bytesUploaded = 0;
|
||||
item.files = files;
|
||||
item.needle = 0;
|
||||
item.percent = 0;
|
||||
item.relatvePaths = relatvePaths;
|
||||
item.size = 0;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
item.size += files[i].size;
|
||||
}
|
||||
this.uploads.push(item);
|
||||
this.uploadNext();
|
||||
}
|
||||
|
||||
public resizePty(appId: string, cols: number, rows: number, wp: number, hp: number) {
|
||||
return this.http.post(environment.BASE_URL + "app/terminal/" + appId + "/resize", { "row": rows, "col": cols, "wp": wp, "hp": hp }).subscribe((rest: any) => {
|
||||
console.log("Resized successfully");
|
||||
});
|
||||
}
|
||||
|
||||
public getHomeChildren(): Observable<any> {
|
||||
return this.http.get(environment.BASE_URL + "app/folder/tree/home");
|
||||
}
|
||||
|
||||
public getDirectoryChildren(path: string): Observable<any> {
|
||||
return this.http.get(environment.BASE_URL + "app/folder/tree/path/" + btoa(path));
|
||||
}
|
||||
|
||||
public getFsChildren(): Observable<any> {
|
||||
return this.http.get(environment.BASE_URL + "app/folder/tree/fs");
|
||||
}
|
||||
|
||||
public initMoveOrCopy(targetFolder: string, files: string[], cut: boolean) {
|
||||
return this.http.post(environment.BASE_URL + "app/fs/" + cut, { "sourceFile": files, "targetFolder": targetFolder }).subscribe((resp: any) => {
|
||||
let fileop: FileOperationItem = new FileOperationItem();
|
||||
fileop.id = resp.id;
|
||||
fileop.name = resp.name;
|
||||
this.fileOperations.push(fileop);
|
||||
console.log("Resized successfully");
|
||||
if (cut) {
|
||||
this.copiedFilePath.files = [];
|
||||
this.copiedFilePath.cut = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public updateFileOpProgress() {
|
||||
let idList: string[] = [];
|
||||
for (let item of this.fileOperations) {
|
||||
idList.push(item.id);
|
||||
}
|
||||
this.http.post(environment.BASE_URL + "app/fs/progress", idList).subscribe((resp: any[]) => {
|
||||
console.log("update: " + JSON.stringify(resp));
|
||||
for (let item of resp) {
|
||||
let fileop: FileOperationItem = this.findFileOpItem(item.id);
|
||||
if (fileop) {
|
||||
fileop.progress = item.progress;
|
||||
fileop.hasError = item.hasError;
|
||||
fileop.name = item.name;
|
||||
fileop.errors = item.errors;
|
||||
if (item.status == "Finished") {
|
||||
for (let i = 0; i < this.fileOperations.length; i++) {
|
||||
if (this.fileOperations[i] === fileop) {
|
||||
this.fileOperations.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
findFileOpItem(id: string) {
|
||||
for (let item of this.fileOperations) {
|
||||
if (item.id === id) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cancelFileOperation(id: string) {
|
||||
return this.http.post(environment.BASE_URL + "app/fs/cancel/" + id, {}).subscribe((rest: any) => {
|
||||
for (let item of this.fileOperations) {
|
||||
for (let i = 0; i < this.fileOperations.length; i++) {
|
||||
if (this.fileOperations[i].id === id) {
|
||||
this.fileOperations.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("cancellation request sent successfully");
|
||||
});
|
||||
}
|
||||
|
||||
renameFile(newName: string, oldName: string, folder: string): Observable<any> {
|
||||
return this.http.post(environment.BASE_URL + "app/fs/rename", { oldName: oldName, newName: newName, folder: folder });
|
||||
}
|
||||
|
||||
deleteFiles(items: string[]): Observable<any> {
|
||||
return this.http.post(environment.BASE_URL + "app/fs/delete", items);
|
||||
}
|
||||
|
||||
getText(path: string): Observable<string> {
|
||||
return this.http.get<string>(environment.BASE_URL + "app/fs/files/" + btoa(path), { responseType: 'text' as 'json' });
|
||||
}
|
||||
|
||||
setText(key: string): Observable<any> {
|
||||
return this.http.post<any>(environment.BASE_URL + "app/fs/files/" + btoa(key), this.editorContexts[key].session.getValue(), {
|
||||
headers: {
|
||||
"Content-Type": "text/plain"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
startSearch(folder: string, searchText: string): Observable<any> {
|
||||
return this.http.post<any>(environment.BASE_URL + "app/fs/search", { folder, searchText });
|
||||
}
|
||||
|
||||
getSearchResults(id: string, folderIndex = 0, fileIndex = 0): Observable<any> {
|
||||
return this.http.get(environment.BASE_URL + "app/fs/search/" + id + "?fileIndex=" + fileIndex + "&folderIndex=" + folderIndex);
|
||||
}
|
||||
|
||||
cancelSearch(id: string): Observable<any> {
|
||||
return this.http.delete(environment.BASE_URL + "app/fs/search/" + id);
|
||||
}
|
||||
|
||||
getPermissionDetails(file: string): Observable<PosixPermissions> {
|
||||
return this.http.get<PosixPermissions>(environment.BASE_URL + "app/fs/posix/" + btoa(file));
|
||||
}
|
||||
|
||||
setPermissionDetails(file: string, perm: PosixPermissions): Observable<any> {
|
||||
return this.http.post<any>(environment.BASE_URL + "app/fs/posix/" + btoa(file), perm);
|
||||
}
|
||||
|
||||
getJwtToken() {
|
||||
if (!this.jwtToken) {
|
||||
this.jwtToken = window.localStorage.getItem("easy-web-shell.jwt-token");
|
||||
}
|
||||
return this.jwtToken;
|
||||
}
|
||||
|
||||
signIn(user: string, pass: string): Observable<any> {
|
||||
return this.http.post<any>(environment.BASE_URL + "token", {}, { headers: { "Authorization": "Basic " + btoa(user + ":" + pass) } });
|
||||
}
|
||||
|
||||
setToken(token: string, remember: boolean) {
|
||||
this.jwtToken = token;
|
||||
if (remember) {
|
||||
window.localStorage.setItem("easy-web-shell.jwt-token", token);
|
||||
}
|
||||
}
|
||||
|
||||
checkToken(): Observable<any> {
|
||||
return this.http.post<any>(environment.BASE_URL + "auth", {});
|
||||
}
|
||||
|
||||
clearOldToken() {
|
||||
this.jwtToken = null;
|
||||
window.localStorage.removeItem("easy-web-shell.jwt-token");
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
return this.http.get<any>(environment.BASE_URL + "app/config");
|
||||
}
|
||||
|
||||
setConfig(config: any) {
|
||||
return this.http.post<any>(environment.BASE_URL + "app/config", config);
|
||||
}
|
||||
|
||||
mkdir(dir: string, name: string): Observable<any> {
|
||||
return this.http.post<any>(environment.BASE_URL + "app/fs/mkdir", { name, dir });
|
||||
}
|
||||
|
||||
touch(dir: string, name: string): Observable<any> {
|
||||
return this.http.post<any>(environment.BASE_URL + "app/fs/touch", { name, dir });
|
||||
}
|
||||
|
||||
getTempToken():Observable<any> {
|
||||
return this.http.get<any>(environment.BASE_URL + "token/temp");
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { TestBed, async, inject } from '@angular/core/testing';
|
||||
|
||||
import { AuthGuardGuard } from './auth-guard.guard';
|
||||
|
||||
describe('AuthGuardGuard', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [AuthGuardGuard]
|
||||
});
|
||||
});
|
||||
|
||||
it('should ...', inject([AuthGuardGuard], (guard: AuthGuardGuard) => {
|
||||
expect(guard).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { DataService } from '../data.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthGuardGuard implements CanActivate {
|
||||
private sub = new Subject<boolean>();
|
||||
constructor(private router: Router, private service: DataService) {
|
||||
}
|
||||
canActivate(
|
||||
next: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
|
||||
|
||||
if (this.service.getJwtToken()) {
|
||||
this.service.checkToken().subscribe((resp: any) => {
|
||||
this.sub.next(true);
|
||||
}, (err: any) => {
|
||||
this.sub.next(false);
|
||||
this.service.clearOldToken();
|
||||
this.router.navigate(["/login"]);
|
||||
});
|
||||
return this.sub;
|
||||
} else {
|
||||
this.router.navigate(["/login"]);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
.desktop-tabs {
|
||||
display: none;
|
||||
border-bottom: 1px solid rgb(230, 230, 230);
|
||||
cursor: pointer;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.mobile-tabs{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media(min-width: 800px){
|
||||
[class="desktop-tabs"]{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
[class="mobile-tabs"]{
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<div style="height: calc(100vh - 55px); width: 100vw; display: flex; flex-direction: column; position: fixed; top: 55px; left: 0px; z-index: 100; background: white;"
|
||||
*ngIf="tabKeys.length>0">
|
||||
<div class="desktop-tabs" *ngIf="service.selectedEditorTab">
|
||||
<div *ngFor="let tab of tabKeys"
|
||||
style="padding: 10px; padding-left: 5px; padding-right: 5px; display: flex; justify-content: space-between;"
|
||||
(click)="loadSession(tab);selectTab(tab)"
|
||||
[ngStyle]="{'background':service.selectedEditorTab==tab?'rgb(230,230,230)':'rgb(245,245,245)'}">
|
||||
<span style="line-height: 30px; margin-left: 10px; margin-right: 10px;">{{getTabName(tab)}}</span>
|
||||
<div style="width: 20px;">
|
||||
<span style="line-height: 30px; width: 20px; text-align: center; color: gray;"
|
||||
(click)="closeTab(tab)"><i class="fa fa-times" aria-hidden="true"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-tabs" *ngIf="service.selectedEditorTab">
|
||||
<select style="flex: 1; border: 1px solid rgb(230,230,230);" (change)="selectItem($event.target.value)">
|
||||
<option *ngFor="let tab of tabKeys" [value]="tab" [selected]="service.selectedEditorTab==tab">
|
||||
{{getTabName(tab)}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="code-editor" #codeEditor style="overflow: auto; min-height: 100px; flex: 1;">
|
||||
</div>
|
||||
<div style="border-top: 1px solid rgb(230,230,230); padding: 10px; display: flex;">
|
||||
<button class="btn btn-primary mr-2" type="button" (click)="save()" [disabled]="saving">
|
||||
<span *ngIf="saving" class="spinner-border spinner-border-sm" role="status"
|
||||
aria-hidden="true"></span>
|
||||
{{saving?"Saving...":"Save"}}
|
||||
</button>
|
||||
<!-- <button class="btn btn-primary mr-2" (click)="save()">Save</button> -->
|
||||
<button class="btn btn-secondary" (click)="close()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="tabKeys.length<1"
|
||||
style="height: calc(100vh - 55px); width: 100vw; position: fixed; top: 55px; left: 0px; z-index: 100; background: white; text-align: center; padding-top: 30px;">
|
||||
<span>No file is opened, please goto files and select some.</span>
|
||||
</div>
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { EditorComponent } from './editor.component';
|
||||
|
||||
describe('EditorComponent', () => {
|
||||
let component: EditorComponent;
|
||||
let fixture: ComponentFixture<EditorComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ EditorComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EditorComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,162 @@
|
|||
import { Component, OnInit, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
|
||||
|
||||
import * as ace from 'ace-builds';
|
||||
//import 'ace-builds/src-noconflict/mode-javascript';
|
||||
import 'ace-builds/src-noconflict/theme-github';
|
||||
|
||||
import 'ace-builds/src-noconflict/ext-language_tools';
|
||||
import 'ace-builds/src-noconflict/ext-beautify';
|
||||
import { DataService } from 'src/app/data.service';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
|
||||
const THEME = 'ace/theme/github';
|
||||
const LANG = 'ace/mode/javascript';
|
||||
|
||||
@Component({
|
||||
selector: 'app-editor',
|
||||
templateUrl: './editor.component.html',
|
||||
styleUrls: ['./editor.component.css'],
|
||||
host: {
|
||||
'(window:resize)': 'onResize($event)'
|
||||
}
|
||||
})
|
||||
export class EditorComponent implements OnInit,AfterViewInit {
|
||||
|
||||
@ViewChild('codeEditor') codeEditorElmRef: ElementRef;
|
||||
private codeEditor: ace.Ace.Editor;
|
||||
|
||||
tabKeys: string[] = [];
|
||||
saving: boolean;
|
||||
|
||||
constructor(public service: DataService, private route: ActivatedRoute) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.getTabbedSessions();
|
||||
// console.log("selected tab: " + this.service.selectedEditorTab+" tabkexs: "+this.tabKeys);
|
||||
if(this.tabKeys.length<1){
|
||||
return;
|
||||
}
|
||||
// if(!this.codeEditorElmRef){
|
||||
// return;
|
||||
// }
|
||||
// console.log("editor loaded");
|
||||
|
||||
|
||||
// this.codeEditor.on("change", ()=> {
|
||||
// let doc:any = this.codeEditor.getSession().getDocument();
|
||||
// let r:any=this.codeEditor.renderer;
|
||||
// let lineHeight = r.lineHeight;
|
||||
// console.log("doc.getLength(): "+doc.getLength()+" lineHeight: "+lineHeight)
|
||||
// this.codeEditorElmRef.nativeElement.style.height = +lineHeight * +doc.getLength() + "px";
|
||||
// this.codeEditor.resize();
|
||||
|
||||
// this.codeEditor.setOption("maxLines", doc.getLength());
|
||||
//});
|
||||
|
||||
// this.route.params.subscribe((params: ParamMap) => {
|
||||
// // console.log("route init of editor: "+JSON.stringify(Object.keys(this.service.editorContexts)));
|
||||
// // console.log("loading tabs")
|
||||
|
||||
// });
|
||||
|
||||
|
||||
}
|
||||
|
||||
ngAfterViewInit(){
|
||||
ace.require('ace/ext/language_tools');
|
||||
if(!this.codeEditorElmRef){
|
||||
return;
|
||||
}
|
||||
const element = this.codeEditorElmRef.nativeElement;
|
||||
const editorOptions = this.getEditorOptions();
|
||||
|
||||
this.codeEditor = ace.edit(element, editorOptions);
|
||||
|
||||
this.codeEditor.setTheme(THEME);
|
||||
//this.codeEditor.getSession().setMode(LANG);
|
||||
this.codeEditor.setShowFoldWidgets(true); // for the scope fold feature
|
||||
this.loadSession(this.service.selectedEditorTab);
|
||||
}
|
||||
|
||||
// missing propery on EditorOptions 'enableBasicAutocompletion' so this is a wolkaround still using ts
|
||||
private getEditorOptions(): Partial<ace.Ace.EditorOptions> & { enableBasicAutocompletion?: boolean; } {
|
||||
const basicEditorOptions: Partial<ace.Ace.EditorOptions> = {
|
||||
highlightActiveLine: true,
|
||||
// minLines: 50,
|
||||
// maxLines: Infinity,
|
||||
autoScrollEditorIntoView: true
|
||||
};
|
||||
|
||||
const extraEditorOptions = {
|
||||
enableBasicAutocompletion: true
|
||||
};
|
||||
const margedOptions = Object.assign(basicEditorOptions, extraEditorOptions);
|
||||
return margedOptions;
|
||||
}
|
||||
|
||||
getTabbedSessions(): void {
|
||||
this.tabKeys = Object.keys(this.service.editorContexts);
|
||||
// this.service.editorContexts.forEach((value: EditorContext, key: string) => {
|
||||
// this.tabKeys.push(key);
|
||||
// console.log("tabbed key: " + key);
|
||||
// });
|
||||
}
|
||||
|
||||
selectItem(id: string) {
|
||||
console.log(id)
|
||||
this.loadSession(id);
|
||||
this.selectTab(id);
|
||||
}
|
||||
|
||||
getTabName(key: string): string {
|
||||
return this.service.editorContexts[key].name;
|
||||
}
|
||||
|
||||
loadSession(key: string) {
|
||||
if (key) {
|
||||
this.codeEditor.setSession(this.service.editorContexts[key].session);
|
||||
}
|
||||
}
|
||||
|
||||
selectTab(key: string) {
|
||||
this.service.selectedEditorTab = key;
|
||||
}
|
||||
|
||||
onResize(event: any) {
|
||||
//console.log("window resized");
|
||||
let doc: any = this.codeEditor.getSession().getDocument();
|
||||
let r: any = this.codeEditor.renderer;
|
||||
let lineHeight = r.lineHeight;
|
||||
//console.log("doc.getLength(): "+doc.getLength()+" lineHeight: "+lineHeight)
|
||||
this.codeEditorElmRef.nativeElement.style.height = +lineHeight * +doc.getLength() + "px";
|
||||
this.codeEditor.resize();
|
||||
}
|
||||
|
||||
closeTab(key: string) {
|
||||
let index: number = -1;
|
||||
for (let i = 0; i < this.tabKeys.length; i++) {
|
||||
if (this.tabKeys[i] == key) {
|
||||
index = i;
|
||||
this.tabKeys.splice(i, 1);
|
||||
delete this.service.editorContexts[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index >= this.tabKeys.length && this.tabKeys.length > 0) {
|
||||
this.service.selectedEditorTab = this.tabKeys[this.tabKeys.length - 1];
|
||||
this.loadSession(this.service.selectedEditorTab);
|
||||
}
|
||||
}
|
||||
|
||||
save() {
|
||||
this.saving = true;
|
||||
this.service.setText(this.service.selectedEditorTab).subscribe((resp: any) => {
|
||||
console.log("save done");
|
||||
this.saving = false;
|
||||
});
|
||||
}
|
||||
|
||||
close(){
|
||||
this.closeTab(this.service.selectedEditorTab);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BrowserComponent } from './browser.component';
|
||||
|
||||
describe('BrowserComponent', () => {
|
||||
let component: BrowserComponent;
|
||||
let fixture: ComponentFixture<BrowserComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ BrowserComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BrowserComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { TabItem } from 'src/app/model/tab-item';
|
||||
|
||||
@Component({
|
||||
selector: 'app-browser',
|
||||
templateUrl: './browser.component.html',
|
||||
styleUrls: ['./browser.component.css']
|
||||
})
|
||||
export class BrowserComponent implements OnInit {
|
||||
|
||||
tabs:TabItem;
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 10px;
|
||||
overflow: hidden;
|
||||
text-overflow: clip;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.width-200px {
|
||||
max-width: 200px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.info-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.info-wrapper-container {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@media (min-width:800px) {
|
||||
[class="info-wrapper-container"] {
|
||||
margin: auto;
|
||||
box-shadow: 5px 5px 5px black;
|
||||
}
|
||||
[class="info-wrapper"] {
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
<div *ngIf="info"
|
||||
style="position: fixed; width: 100vw; max-width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); display: flex; justify-content: center; left: 0px; top: 0px; z-index: 150;">
|
||||
<div class="info-wrapper-container">
|
||||
<div class="info-wrapper">
|
||||
<div style="padding-bottom: 10px; ">
|
||||
<span style="font-size: 20px;">Information</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span style="padding-right: 20px; max-width: 400px;">Name</span>
|
||||
<span>{{info.name}}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span style="padding-right: 20px;">Type</span>
|
||||
<span>{{info.type}}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span style="padding-right: 20px;">Size</span>
|
||||
<span>{{info.size}}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span style="padding-right: 20px;">Modified date</span>
|
||||
<span>{{info.lastModified|date:'medium'}}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span style="padding-right: 20px;">Creation date</span>
|
||||
<span>{{info.lastModified|date:'medium'}}</span>
|
||||
</div>
|
||||
<div *ngIf="posixPerm" class="info-item">
|
||||
<span style="padding-right: 20px;">Owner</span>
|
||||
<span>{{posixPerm.owner}}</span>
|
||||
</div>
|
||||
<div *ngIf="posixPerm" class="info-item">
|
||||
<span style="padding-right: 20px;">Owner access</span>
|
||||
<select class="form-control width-200px" [(ngModel)]="posixPerm.ownerAccess">
|
||||
<option value="READ_ONLY">Read-only</option>
|
||||
<option value="WRITE_ONLY">Write-only</option>
|
||||
<option value="READ_WRITE">Read and write</option>
|
||||
<option value="NONE">None</option>
|
||||
</select>
|
||||
</div>
|
||||
<div *ngIf="posixPerm" class="info-item">
|
||||
<span style="padding-right: 20px;">Group</span>
|
||||
<span>{{posixPerm.group}}</span>
|
||||
</div>
|
||||
<div *ngIf="posixPerm" class="info-item">
|
||||
<span style="padding-right: 20px;">Group access</span>
|
||||
<select class="form-control width-200px" [(ngModel)]="posixPerm.groupAccess">
|
||||
<option value="READ_ONLY">Read-only</option>
|
||||
<option value="WRITE_ONLY">Write-only</option>
|
||||
<option value="READ_WRITE">Read and write</option>
|
||||
<option value="NONE">None</option>
|
||||
</select>
|
||||
</div>
|
||||
<div *ngIf="posixPerm" class="info-item">
|
||||
<span style="padding-right: 20px;">Others access</span>
|
||||
<select class="form-control width-200px" [(ngModel)]="posixPerm.otherAccess">
|
||||
<option value="READ_ONLY">Read-only</option>
|
||||
<option value="WRITE_ONLY">Write-only</option>
|
||||
<option value="READ_WRITE">Read and write</option>
|
||||
<option value="NONE">None</option>
|
||||
</select>
|
||||
</div>
|
||||
<div *ngIf="posixPerm" class="info-item">
|
||||
<div><input type="checkbox" [(ngModel)]="posixPerm.executable"><span>Executable</span></div>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; padding-top: 10px;">
|
||||
<button *ngIf="posixPerm" class="btn btn-primary mr-2" (click)="saveAndClose()">Save</button>
|
||||
<button class="btn btn-primary" (click)="closeDialog()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { InfoComponent } from './info.component';
|
||||
|
||||
describe('InfoComponent', () => {
|
||||
let component: InfoComponent;
|
||||
let fixture: ComponentFixture<InfoComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ InfoComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(InfoComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
import { Component, OnInit, Input, OnChanges, Output, EventEmitter } from '@angular/core';
|
||||
import { FileInfo } from 'src/app/model/file-info';
|
||||
import { DataService } from 'src/app/data.service';
|
||||
import { PosixPermissions } from 'src/app/model/posix-permissions';
|
||||
import { FileItem } from 'src/app/model/file-item';
|
||||
|
||||
@Component({
|
||||
selector: 'app-info',
|
||||
templateUrl: './info.component.html',
|
||||
styleUrls: ['./info.component.css']
|
||||
})
|
||||
export class InfoComponent implements OnInit {
|
||||
|
||||
info: FileItem;
|
||||
posixPerm: PosixPermissions;
|
||||
|
||||
@Output()
|
||||
dialogClosed = new EventEmitter<any>();
|
||||
|
||||
constructor(private service: DataService) { }
|
||||
|
||||
ngOnInit() {
|
||||
for (let i = 0; i < this.service.tabs[this.service.selectedTab].files.length; i++) {
|
||||
if (this.service.tabs[this.service.selectedTab].files[i].selected) {
|
||||
this.info = this.service.tabs[this.service.selectedTab].files[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (this.info) {
|
||||
this.service.getPermissionDetails(this.info.path).subscribe((resp: PosixPermissions) => {
|
||||
this.posixPerm = resp;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
saveAndClose() {
|
||||
this.service.setPermissionDetails(this.info.path, this.posixPerm).subscribe((resp: PosixPermissions) => {
|
||||
this.dialogClosed.emit({});
|
||||
});
|
||||
}
|
||||
|
||||
closeDialog() {
|
||||
this.dialogClosed.emit({});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<div
|
||||
style="position: fixed; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); display: flex; justify-content: center; left: 0px; top: 0px; z-index: 150;">
|
||||
<div style="margin: auto; box-shadow: 5px 5px 5px black;">
|
||||
<div style="display: flex; flex-direction: column; background: white; padding: 20px; border-radius: 5px;">
|
||||
<div class="form-group">
|
||||
<label for="txt">New {{itemType}}</label>
|
||||
<input type="text" class="form-control" id="txt" placeholder="Name of the file or folder" [(ngModel)]="fileName">
|
||||
</div>
|
||||
<div style="display: flex;">
|
||||
<button class="btn btn-primary mr-2" style="flex: 1;" (click)="createItem()">Create</button>
|
||||
<button class="btn btn-secondary" style="flex: 1;" (click)="cancelDialog()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NewItemComponent } from './new-item.component';
|
||||
|
||||
describe('NewItemComponent', () => {
|
||||
let component: NewItemComponent;
|
||||
let fixture: ComponentFixture<NewItemComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ NewItemComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(NewItemComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-new-item',
|
||||
templateUrl: './new-item.component.html',
|
||||
styleUrls: ['./new-item.component.css']
|
||||
})
|
||||
export class NewItemComponent implements OnInit {
|
||||
|
||||
@Input()
|
||||
itemType: string;
|
||||
fileName: string;
|
||||
|
||||
@Output()
|
||||
dialogCancelled = new EventEmitter<any>();
|
||||
|
||||
@Output()
|
||||
fileNameEntered = new EventEmitter<string>();
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
createItem() {
|
||||
this.fileNameEntered.emit(this.fileName);
|
||||
}
|
||||
|
||||
cancelDialog() {
|
||||
this.dialogCancelled.emit();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<div
|
||||
style="position: fixed; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); display: flex; justify-content: center; left: 0px; top: 0px; z-index: 150;">
|
||||
<div style="margin: auto; box-shadow: 5px 5px 5px black;">
|
||||
<div style="display: flex; flex-direction: column; background: white; padding: 20px; border-radius: 5px;">
|
||||
<div class="form-group">
|
||||
<label for="txt">Rename</label>
|
||||
<input type="text" class="form-control" id="txt" placeholder="New name of the file or folder" [(ngModel)]="newFileName">
|
||||
</div>
|
||||
<div style="display: flex;">
|
||||
<button class="btn btn-primary mr-2" style="flex: 1;" (click)="renameFile()">OK</button>
|
||||
<button class="btn btn-secondary" style="flex: 1;" (click)="cancelDialog()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RenameComponent } from './rename.component';
|
||||
|
||||
describe('RenameComponent', () => {
|
||||
let component: RenameComponent;
|
||||
let fixture: ComponentFixture<RenameComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ RenameComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(RenameComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
import { Component, OnInit, Input, OnChanges, Output, EventEmitter } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { DataService } from 'src/app/data.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-rename',
|
||||
templateUrl: './rename.component.html',
|
||||
styleUrls: ['./rename.component.css']
|
||||
})
|
||||
export class RenameComponent implements OnInit {
|
||||
|
||||
@Output()
|
||||
fileNameChanged = new EventEmitter<string>();
|
||||
|
||||
@Output()
|
||||
dialogCancelled = new EventEmitter<any>();
|
||||
|
||||
newFileName: string;
|
||||
|
||||
constructor(private service:DataService) { }
|
||||
|
||||
ngOnInit() {
|
||||
for (let i = 0; i < this.service.tabs[this.service.selectedTab].files.length; i++) {
|
||||
if (this.service.tabs[this.service.selectedTab].files[i].selected) {
|
||||
this.newFileName = this.service.tabs[this.service.selectedTab].files[i].name;
|
||||
console.log("new filename: "+this.newFileName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renameFile() {
|
||||
this.fileNameChanged.emit(this.newFileName);
|
||||
}
|
||||
|
||||
cancelDialog() {
|
||||
this.dialogCancelled.emit();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
.nav-tree {
|
||||
height: 100%;
|
||||
width: 250px;
|
||||
min-width: 250px;
|
||||
background: rgb(240, 240, 240);
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid rgb(230, 230, 230);
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
position: relative;
|
||||
max-width: calc(100vw - 250px);
|
||||
}
|
||||
|
||||
.file-list-content-wrapper {
|
||||
padding-top: 52px;
|
||||
}
|
||||
|
||||
.file-list-header {
|
||||
display: flex;
|
||||
height: 51px;
|
||||
background: white;
|
||||
border-bottom: 1px solid rgb(230, 230, 230);
|
||||
cursor: pointer;
|
||||
position: fixed;
|
||||
box-shadow: 0px 0px 6px 0px rgb(230, 230, 230);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.file-list-item-row {
|
||||
display: flex;
|
||||
height: 51px;
|
||||
border-bottom: 1px solid rgb(240, 240, 240);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.desktop-file-list-first-item {
|
||||
flex: 2;
|
||||
overflow-x: hidden;
|
||||
line-height: 50px;
|
||||
display: flex;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.desktop-file-list-other-item {
|
||||
flex: 1;
|
||||
line-height: 50px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-file-list-row {
|
||||
display: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.desktop-nav {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
background: rgb(245, 245, 245);
|
||||
border-bottom: 1px solid rgb(230, 230, 230);
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mobile-nav {
|
||||
display: none;
|
||||
justify-content: space-around;
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
background: rgb(245, 245, 245);
|
||||
border-top: 1px solid rgb(230, 230, 230);
|
||||
}
|
||||
|
||||
.desktop-file-tabs {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
background: rgb(230, 230, 230);
|
||||
}
|
||||
|
||||
.dropdown-ctx-menu {
|
||||
display: flex;
|
||||
width: 200px;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
right: 0px;
|
||||
max-height: 50vh;
|
||||
background: white;
|
||||
border: 1px solid rgb(200, 200, 200);
|
||||
box-shadow: 3px 3px 10px 0px rgb(200, 200, 200);
|
||||
z-index: 301;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.noselect {
|
||||
-webkit-touch-callout: none;
|
||||
/* iOS Safari */
|
||||
-webkit-user-select: none;
|
||||
/* Safari */
|
||||
-khtml-user-select: none;
|
||||
/* Konqueror HTML */
|
||||
-moz-user-select: none;
|
||||
/* Firefox */
|
||||
-ms-user-select: none;
|
||||
/* Internet Explorer/Edge */
|
||||
user-select: none;
|
||||
/* Non-prefixed version, currently
|
||||
supported by Chrome and Opera */
|
||||
}
|
||||
|
||||
@media (max-width:800px) {
|
||||
[class="nav-tree"] {
|
||||
display: none;
|
||||
}
|
||||
[class="file-list"] {
|
||||
max-width: 100vw;
|
||||
}
|
||||
[class="file-list-header"] {
|
||||
display: none;
|
||||
}
|
||||
[class*="file-list-content-wrapper"] {
|
||||
padding-top: 0px;
|
||||
}
|
||||
[class*="file-list-item-row"] {
|
||||
height: 80px;
|
||||
}
|
||||
[class="desktop-file-list-first-item"] {
|
||||
display: none;
|
||||
}
|
||||
[class="desktop-file-list-other-item"] {
|
||||
display: none;
|
||||
}
|
||||
[class="mobile-file-list-row"] {
|
||||
display: flex;
|
||||
}
|
||||
[class="desktop-file-list-other-item"] {
|
||||
display: none;
|
||||
}
|
||||
[class="desktop-nav"] {
|
||||
display: none;
|
||||
}
|
||||
[class="mobile-nav"] {
|
||||
display: flex;
|
||||
}
|
||||
[class*="desktop-file-tabs"] {
|
||||
display: none;
|
||||
}
|
||||
[class="dropdown-ctx-menu"] {
|
||||
bottom: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width:800px) {
|
||||
[class="dropdown-ctx-menu"] {
|
||||
top: 100px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,366 @@
|
|||
<div style="height: 100%; width: 100%; display: flex;" (click)="onWindowClicked(contextmenu)" class="noselect">
|
||||
<!-- navigation tree start -->
|
||||
<div class="nav-tree">
|
||||
<!-- <app-tree [model]="service.tree1" [icon]="'fa-bookmark-o'"></app-tree> -->
|
||||
<app-tree [model]="service.tree2" [icon]="'fa-folder-o'"></app-tree>
|
||||
<app-tree [model]="service.tree3" [icon]="'fa-hdd-o'"></app-tree>
|
||||
</div>
|
||||
|
||||
<!-- navigation tree end -->
|
||||
|
||||
<div *ngIf="service.tabs&&service.tabs.length>0" style="flex: 1; display: flex; flex-direction: column;">
|
||||
|
||||
<!-- tabbed pane for folders start -->
|
||||
<div *ngIf="service.tabs.length>1" class="desktop-file-tabs">
|
||||
<div (click)="showTab(i)" (mouseenter)="hoverIndex=i" (mouseleave)="hoverIndex=-1"
|
||||
*ngFor="let tab of service.tabs; let i=index"
|
||||
[ngStyle]="{'background': service.selectedTab==i? 'rgb(245,245,245)' : 'rgb(230,230,230)'}"
|
||||
style="padding: 10px; padding-left: 10px; padding-right: 5px; display: flex; justify-content: space-between; cursor: pointer;">
|
||||
<span style="line-height: 30px; margin-left: 10px; margin-right: 10px;">{{tab.folderName}}</span>
|
||||
<div style="width: 20px;">
|
||||
<span style="line-height: 30px; width: 20px; text-align: center; color: gray;" *ngIf="hoverIndex==i"
|
||||
(click)="closeTab(i)"><i class="fa fa-times" aria-hidden="true"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- tabbed pane for folders end -->
|
||||
|
||||
<!-- desktop folder navigation components start -->
|
||||
|
||||
<div class="desktop-nav">
|
||||
<div style="flex: 1; margin-right: 10px;">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text" (click)="navigateUp()" style="cursor: pointer;" placement="right"
|
||||
ngbTooltip="Up one level"><i class="fa fa-arrow-up"></i></span>
|
||||
<span class="input-group-text" style="cursor: pointer;" (click)="navigateHome()" placement="right"
|
||||
ngbTooltip="Home"><i class="fa fa-home"></i></span>
|
||||
</div>
|
||||
<input type="text" class="form-control" (keyup.enter)="navigateTo(address.value)"
|
||||
[value]="service.tabs[service.selectedTab].currentDirectory" #address>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text" (click)="navigateTo(address.value)" placement="left" ngbTooltip="Go"><i
|
||||
class="fa fa-arrow-right"></i></span>
|
||||
<span class="input-group-text" (click)="navigateTo(service.tabs[service.selectedTab].currentDirectory)"
|
||||
placement="left" ngbTooltip="Refresh">
|
||||
<i class="fa fa-refresh"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-right: 10px;">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend" style="position: relative;">
|
||||
<span class="input-group-text" (click)="uploadPopup=!uploadPopup; $event.stopPropagation()"
|
||||
style="cursor: pointer; padding: 10px;" placement="left" ngbTooltip="Upload">
|
||||
<i class="fa fa-upload"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text" style="cursor: pointer; padding: 10px;" (click)="downloadFiles()"
|
||||
placement="left" ngbTooltip="Download">
|
||||
<i class="fa fa-download"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="uploadPopup"
|
||||
style="position: fixed; z-index: 30; background: white; box-shadow: 3px 3px rgb(240,240,240);"
|
||||
(focusout)="uploadPopup=false">
|
||||
<div class="context-menu-items" (click)="file.click(); uploadPopup=false"><span>Upload
|
||||
file</span>
|
||||
</div>
|
||||
<div class="context-menu-items" (click)="folder.click(); uploadPopup=false"><span>Upload
|
||||
folder</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text" (click)="newFile()" style="cursor: pointer; padding: 10px;" placement="left"
|
||||
ngbTooltip="New file">
|
||||
<i class="fa fa-file" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="input-group-text" (click)="newFolder()" style="cursor: pointer; padding: 10px;"
|
||||
placement="left" ngbTooltip="New folder">
|
||||
<i class="fa fa-folder" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="input-group-text" (click)="deleteFiles()" style="cursor: pointer; padding: 10px;"
|
||||
placement="left" ngbTooltip="Delete">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text" (click)="showDropDownMenu(contextmenu,$event)"
|
||||
style="cursor: pointer; padding: 10px;" placement="left" ngbTooltip="More">
|
||||
<i class="fa fa-ellipsis-h"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- desktop folder navigation components end -->
|
||||
|
||||
<div class="file-list" *ngIf="service.tabs[service.selectedTab]&&service.tabs[service.selectedTab].files">
|
||||
<!-- header start -->
|
||||
<div [style.width]="content.clientWidth+'px'" class="file-list-header" #header>
|
||||
<span style="flex: 2; line-height: 50px; padding-left: 20px;" (click)="sort(0)">
|
||||
<span style="padding-right: 10px;">Name</span>
|
||||
<span *ngIf="this.sortColumn==0&&this.ascendingSort">
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span *ngIf="this.sortColumn==0&&!this.ascendingSort">
|
||||
<i class="fa fa-caret-up" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span style="flex: 1; line-height: 50px;" (click)="sort(1)">
|
||||
<span style="padding-right: 10px;">
|
||||
Size
|
||||
</span>
|
||||
<span *ngIf="this.sortColumn==1&&this.ascendingSort">
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span *ngIf="this.sortColumn==1&&!this.ascendingSort">
|
||||
<i class="fa fa-caret-up" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span style="flex: 1; line-height: 50px;" (click)="sort(2)">
|
||||
<span style="padding-right: 10px;">Modified</span>
|
||||
<span *ngIf="this.sortColumn==2&&this.ascendingSort">
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span *ngIf="this.sortColumn==2&&!this.ascendingSort">
|
||||
<i class="fa fa-caret-up" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span *ngIf="posix" style="flex: 1; line-height: 50px;" (click)="sort(3)">
|
||||
<span style="padding-right: 10px;">
|
||||
Permission
|
||||
</span>
|
||||
<span *ngIf="this.sortColumn==3&&this.ascendingSort">
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span *ngIf="this.sortColumn==3&&!this.ascendingSort">
|
||||
<i class="fa fa-caret-up" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span *ngIf="posix" style="flex: 1; line-height: 50px;" (click)="sort(4)">
|
||||
<span>
|
||||
Owner
|
||||
</span>
|
||||
<span *ngIf="this.sortColumn==4&&this.ascendingSort">
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span *ngIf="this.sortColumn==4&&!this.ascendingSort">
|
||||
<i class="fa fa-caret-up" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<!-- header end -->
|
||||
|
||||
<!-- actual content begin -->
|
||||
|
||||
<div #content class="file-list-content-wrapper" (keydown)="keyup($event)" id="list" #list>
|
||||
<div *ngFor="let file of service.tabs[service.selectedTab].files; let i=index" class="file-list-item-row"
|
||||
[style.color]="file.selected?'white':'black'" [style.background]="file.selected?'deepskyblue':'white'"
|
||||
(dblclick)="file.type === 'Directory'?navigateTo(file.path): openItem(file)"
|
||||
(click)="itemClicked(file,$event.ctrlKey, divitem.className)" tabindex="0" (keydown)="onKeyDown($event)"
|
||||
(contextmenu)="onContextMenu($event, file, contextmenu, file)" #divitem>
|
||||
|
||||
<!-- desktop layout for row start -->
|
||||
<div class="desktop-file-list-first-item">
|
||||
<span style="line-height: 50px; font-size: 30px; padding-left: 15px; padding-right: 15px; color: gray;"><i
|
||||
[class]="getFileIcon(file)" aria-hidden="true"></i></span>
|
||||
|
||||
<!-- <img [src]="(file.type=='Directory'||file.type=='DirLink')?'assets/folder.png':'assets/file.png'"
|
||||
style="height: 40px; margin-top: 5px; margin-left: 10px; margin-right: 10px;"> -->
|
||||
<span (mousedown)="preventTextSelection($event)"
|
||||
style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" class="noselect">
|
||||
{{file.name}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="desktop-file-list-other-item">
|
||||
{{(file.type!='Directory'&&file.type!='DirLink')?formatFileSize(file.size):''}}</div>
|
||||
<div class="desktop-file-list-other-item">
|
||||
{{file.lastModified|date:'medium'}}</div>
|
||||
<div *ngIf="posix" class="desktop-file-list-other-item">
|
||||
{{file.permissionString}}</div>
|
||||
<div *ngIf="posix" class="desktop-file-list-other-item">
|
||||
{{file.user}}</div>
|
||||
<!-- desktop layout for row end -->
|
||||
|
||||
<!-- mobile layout for row start -->
|
||||
<div class="mobile-file-list-row">
|
||||
<div
|
||||
style="height: 100%; display: flex; flex-direction: column; align-content: center; justify-content: center;">
|
||||
<span style="font-size: 40px; padding-left: 15px; padding-right: 15px; color: gray;"><i
|
||||
[class]="getFileIcon(file)" aria-hidden="true"></i></span>
|
||||
<!-- <img [src]="(file.type=='Directory'||file.type=='DirLink')?'assets/folder.png':'assets/file.png'"
|
||||
style="height: 40px; margin: auto; padding-left: 5px;"> -->
|
||||
</div>
|
||||
<div
|
||||
style="display: flex; flex-direction: column; flex: 1; height: 100%; align-content: center; justify-content: center;">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; " class="noselect">
|
||||
<span style="padding-left: 5px; font-size: 16px;">
|
||||
{{file.name}}
|
||||
</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<div>
|
||||
<span style="padding-left: 5px; color: gray;">
|
||||
{{file.lastModified|date:'medium'}}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span style="padding-right: 10px; color: gray;">
|
||||
{{(file.type!='Directory'&&file.type!='DirLink')?formatFileSize(file.size):''}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- mobile layout for row end -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- actual content end -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- mobile folder navigation start -->
|
||||
<div class="mobile-nav">
|
||||
<span (click)="navigateUp()" style="cursor: pointer; padding: 10px;"><i class="fa fa-arrow-up"></i></span>
|
||||
<span style="cursor: pointer; padding: 10px;" (click)="navigateHome()"><i class="fa fa-home"></i></span>
|
||||
<span style="cursor: pointer; padding: 10px;"
|
||||
(click)="navigateTo(service.tabs[service.selectedTab].currentDirectory)">
|
||||
<i class="fa fa-refresh"></i></span>
|
||||
<span (click)="file.click();" style="cursor: pointer; padding: 10px;">
|
||||
<i class="fa fa-upload"></i>
|
||||
</span>
|
||||
<span style="cursor: pointer; padding: 10px;" (click)="downloadFiles()">
|
||||
<i class="fa fa-download"></i>
|
||||
</span>
|
||||
<span [style.background]="multipleSelectionMode?'rgb(230,230,230)':'rgb(245,245,245)'"
|
||||
(click)="multipleSelectionMode=!multipleSelectionMode" style="cursor: pointer; padding: 10px;">
|
||||
<i class="fa fa-check" aria-hidden="true"></i>
|
||||
</span>
|
||||
<!-- <span (click)="navigateUp()" style="cursor: pointer; padding: 10px;">
|
||||
<i class="fa fa-file" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span (click)="navigateHome()" style="cursor: pointer; padding: 10px;">
|
||||
<i class="fa fa-folder" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span (click)="navigateHome()" style="cursor: pointer; padding: 10px;">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</span> -->
|
||||
<span style="cursor: pointer; padding: 10px;" (click)="showDropdownCtx=true; $event.stopPropagation()">
|
||||
<i class="fa fa-ellipsis-h"></i>
|
||||
</span>
|
||||
</div>
|
||||
<!-- mobile folder navigation end -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- hidden upload divs start -->
|
||||
<div style="display:none">
|
||||
<input type="file" #file (change)="fileSelectedForUpload($event)">
|
||||
</div>
|
||||
<div style="display:none">
|
||||
<input type="file" webkitdirectory mozdirectory #folder (change)="folderSelectedForUpload($event)">
|
||||
</div>
|
||||
<!-- hidden upload divs end -->
|
||||
|
||||
<!-- file menu start -->
|
||||
<div
|
||||
style="display: none; width: 200px; flex-direction: column;display: none; position: fixed; background: white; border: 1px solid rgb(200,200,200); box-shadow: 3px 3px 10px 0px rgb(200,200,200);"
|
||||
#contextmenu (click)="onWindowClicked(contextmenu)">
|
||||
<div class="context-menu-items" (click)="openInNewTab()">Open in new tab</div>
|
||||
<div class="context-menu-items" (click)="openInTab()">Open</div>
|
||||
<div *ngIf="isAnySelection()" class="context-menu-items" (click)="deleteFiles()">Delete</div>
|
||||
<div *ngIf="isSingleSelection()" class="context-menu-items" (click)="showRenameDialog=true">Rename</div>
|
||||
<div class="context-menu-items" (click)="copyFiles(true)">Cut</div>
|
||||
<div class="context-menu-items" (click)="copyFiles(false)">Copy</div>
|
||||
<div *ngIf="service.copiedFilePath.files&&service.copiedFilePath.files.length>0" class="context-menu-items"
|
||||
(click)="pasteFiles()">Paste</div>
|
||||
<div class="context-menu-items" (click)="showInfo()">Properties</div>
|
||||
</div>
|
||||
<!-- file menu end -->
|
||||
|
||||
|
||||
|
||||
<!-- rename dialog start-->
|
||||
<app-rename *ngIf="showRenameDialog" (dialogCancelled)="showRenameDialog=false"
|
||||
(fileNameChanged)="showRenameDialog=false; renameFile($event);"></app-rename>
|
||||
<!-- rename dialog end-->
|
||||
|
||||
<!-- new folder start -->
|
||||
<app-new-item *ngIf="showNewFolderItem" [itemType]="'folder'" (dialogCancelled)="showNewFolderItem=false"
|
||||
(fileNameEntered)="showNewFolderItem=false; createFolder($event);"></app-new-item>
|
||||
<!-- new folder end -->
|
||||
|
||||
<!-- new file start -->
|
||||
<app-new-item *ngIf="showNewFileItem" [itemType]="'file'" (dialogCancelled)="showNewFileItem=false"
|
||||
(fileNameEntered)="showNewFileItem=false; createFile($event);"></app-new-item>
|
||||
<!-- new file end -->
|
||||
|
||||
<!-- loading indicator start -->
|
||||
<div *ngIf="loading"
|
||||
style="width: 100vw; height: 100vh; position: absolute; left: 0px; top: 0px; background: rgba(255,255,255,0.5); z-index: 20; display: flex; justify-content: center; overflow: hidden;">
|
||||
<div class="spinner-border text-primary" role="status"
|
||||
style="margin: auto; width: 5rem; height: 5rem; font-size: 30px;">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- loading indicator end -->
|
||||
|
||||
<!-- info dialog start -->
|
||||
<app-info *ngIf="showInfoDialog" (dialogClosed)="showInfoDialog=false"></app-info>
|
||||
<!-- info dialog end -->
|
||||
|
||||
<!-- dropdown context menu start -->
|
||||
<div *ngIf="showDropdownCtx" class="dropdown-ctx-menu" #contextmenu1>
|
||||
<div class="context-menu-items" (click)="newFolder(); onWindowClicked(contextmenu)">New folder</div>
|
||||
<div class="context-menu-items" (click)="newFile(); onWindowClicked(contextmenu)">New file</div>
|
||||
<div *ngIf="isSingleSelection()" class="context-menu-items" (click)="showDropdownCtx=false; openInNewTab()">Open in
|
||||
new tab</div>
|
||||
<div *ngIf="isSingleSelection()" class="context-menu-items" (click)="showDropdownCtx=false; openInTab()">Open</div>
|
||||
<div *ngIf="isAnySelection()" class="context-menu-items" (click)="showDropdownCtx=false; deleteFiles()">Delete
|
||||
</div>
|
||||
<div *ngIf="isSingleSelection()" class="context-menu-items" (click)="showDropdownCtx=false; showRenameDialog=true">
|
||||
Rename</div>
|
||||
<div *ngIf="isSingleSelection()" class="context-menu-items" (click)="showDropdownCtx=false; copyFiles(true)">Cut</div>
|
||||
<div *ngIf="isSingleSelection()" class="context-menu-items" (click)="showDropdownCtx=false; copyFiles(false)">Copy
|
||||
</div>
|
||||
<div *ngIf="service.copiedFilePath.files&&service.copiedFilePath.files.length>0" class="context-menu-items"
|
||||
(click)="showDropdownCtx=false; pasteFiles()">Paste</div>
|
||||
<div *ngIf="isSingleSelection()" class="context-menu-items" (click)="showDropdownCtx=false; showInfo()">Properties
|
||||
</div>
|
||||
</div>
|
||||
<!-- dropdown context menu end -->
|
||||
|
||||
<app-image-viewer [url]="getSelectedFile().path" *ngIf="previewer=='image'" (componentClosed)="previewer=null">
|
||||
</app-image-viewer>
|
||||
<app-media-player [url]="getSelectedFile().path" *ngIf="previewer=='video'" (componentClosed)="previewer=null">
|
||||
</app-media-player>
|
||||
<app-unsupported-content-viewer [url]="getSelectedFile().path" [file]="getSelectedFile().name"
|
||||
*ngIf="previewer=='unsupported'" (componentClosed)="previewer=null" (onDownload)="downloadFiles()"
|
||||
(onOpen)="openAsText();previewer=null">
|
||||
</app-unsupported-content-viewer>
|
||||
|
||||
<div *ngIf="toastVisible"
|
||||
style="background: rgb(0,0,0,0.2); width: 100vw; height: 100vh; position: fixed; z-index: 305; display: flex; flex-direction: column; justify-content: center; align-items: center; left: 0px; top: 0px;">
|
||||
<div
|
||||
style="background: white; min-width: 400px; display: flex; flex-direction: column; border-radius: 5px; box-shadow: 2px 2px 5px 0px gray; padding: 10px;">
|
||||
<span style="padding-top: 20px;padding-bottom: 20px;">{{toastMessage}}</span>
|
||||
<div style="display: flex; flex-direction: row-reverse;">
|
||||
<button class="btn btn-primary" (click)="toastVisible=false">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div *ngIf="toastVisible&&toastMessage"
|
||||
style="position: fixed; bottom: 30px; left: 30px; padding: 5px; padding-left: 20px; padding-right: 20px; background: rgb(30,30,30); border-radius: 5px; box-shadow: 2px 2px 5px 0px black; color: white;">
|
||||
<span>{{toastMessage}}</span>
|
||||
</div> -->
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FilesComponent } from './files.component';
|
||||
|
||||
describe('FilesComponent', () => {
|
||||
let component: FilesComponent;
|
||||
let fixture: ComponentFixture<FilesComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ FilesComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FilesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,772 @@
|
|||
import { Component, OnInit, ViewChild, ElementRef, OnDestroy, Output, EventEmitter } from '@angular/core';
|
||||
import { TabItem } from 'src/app/model/tab-item';
|
||||
import { FileItem } from 'src/app/model/file-item';
|
||||
import { DataService } from 'src/app/data.service';
|
||||
import { Subject, Subscriber, Subscription } from 'rxjs';
|
||||
import { TreeComponent } from './tree/tree.component';
|
||||
import { FolderTab } from 'src/app/model/folder-tab';
|
||||
import { FormControl } from '@angular/forms';
|
||||
|
||||
import { utility } from '../../utility/utils';
|
||||
|
||||
|
||||
import * as ace from 'ace-builds';
|
||||
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { EditorContext } from 'src/app/model/editor-context';
|
||||
|
||||
@Component({
|
||||
selector: 'app-files',
|
||||
templateUrl: './files.component.html',
|
||||
styleUrls: ['./files.component.css'],
|
||||
host: {
|
||||
'(window:resize)': 'onResize($event)'
|
||||
}
|
||||
})
|
||||
export class FilesComponent implements OnInit, OnDestroy {
|
||||
|
||||
toastVisible: boolean = false;
|
||||
toastMessage: string;
|
||||
|
||||
@ViewChild("list")
|
||||
list: ElementRef;
|
||||
|
||||
@ViewChild("header")
|
||||
header: ElementRef;
|
||||
|
||||
@Output()
|
||||
viewChanged = new EventEmitter<string>();
|
||||
|
||||
uploadPopup: boolean;
|
||||
hoverIndex: number = -1;
|
||||
showRenameDialog: boolean = false;
|
||||
loading: boolean = false;
|
||||
newTabListener: Subscription;
|
||||
currentTabListener: Subscription;
|
||||
showInfoDialog: boolean;
|
||||
multipleSelectionMode: boolean = false;
|
||||
showDropdownCtx: boolean = false;
|
||||
previewer: string;
|
||||
sortColumn: number;
|
||||
ascendingSort: boolean;
|
||||
showNewFolderItem: boolean;
|
||||
showNewFileItem: boolean;
|
||||
posix: boolean;
|
||||
|
||||
constructor(public service: DataService, private router: Router) { }
|
||||
|
||||
ngOnInit() {
|
||||
console.log("File browser init")
|
||||
this.posix = this.service.posix;
|
||||
if (this.service.tabs.length < 1) {
|
||||
this.loading = true;
|
||||
this.service.listHome().subscribe((data: any) => {
|
||||
//console.log("files: " + JSON.stringify(data));
|
||||
this.service.tabs.push({
|
||||
files: data.files,
|
||||
currentDirectory: data.folder,
|
||||
selected: false,
|
||||
folderName: data.folderName,
|
||||
posix: data.posix
|
||||
});
|
||||
this.service.selectedTab = 0;
|
||||
this.loading = false;
|
||||
}, err => {
|
||||
this.showError("Unable to list files");
|
||||
this.loading = false;
|
||||
});
|
||||
} else {
|
||||
this.navigateTo(this.service.tabs[this.service.selectedTab].currentDirectory);
|
||||
}
|
||||
|
||||
this.newTabListener = this.service.newTabListener.subscribe((tab: FolderTab) => {
|
||||
this.openNewTab(tab);
|
||||
});
|
||||
|
||||
this.currentTabListener = this.service.currentTabListener.subscribe((path: any) => {
|
||||
if (path.file) {
|
||||
this.openItem(path);
|
||||
} else {
|
||||
this.navigateTo(path.path);
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.newTabListener) {
|
||||
this.newTabListener.unsubscribe();
|
||||
}
|
||||
|
||||
if (this.currentTabListener) {
|
||||
this.currentTabListener.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
openItem(fileItem: FileItem) {
|
||||
let file: string = fileItem.path;
|
||||
this.loading = true;
|
||||
console.log("Opening file: " + file)
|
||||
if (fileItem.type.startsWith("image")) {
|
||||
this.previewer = "image";
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
if (fileItem.type.startsWith("video")) {
|
||||
this.previewer = "video";
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
if (!fileItem.type.startsWith("text")) {
|
||||
this.loading = false;
|
||||
this.previewer = "unsupported";
|
||||
console.log("Previewer: " + this.previewer);
|
||||
return;
|
||||
}
|
||||
this.openWithTextEditor(file);
|
||||
}
|
||||
|
||||
openWithTextEditor(file: string) {
|
||||
this.service.getText(file).subscribe((text: string) => {
|
||||
this.service.selectedEditorTab = file;
|
||||
let ctx = new EditorContext(utility.getFileName(file), file, text);
|
||||
ctx.session.setUseWrapMode(false);
|
||||
this.service.editorContexts[file] = ctx;
|
||||
this.loading = false;
|
||||
console.log("before route init of editor: " + JSON.stringify(Object.keys(this.service.editorContexts)))
|
||||
this.viewChanged.emit("editor");
|
||||
//this.router.navigate(["/app/editor"]);
|
||||
}, err => {
|
||||
this.showError("Unable to open file");
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
openAsText() {
|
||||
let file: string;
|
||||
for (let i = 0; i < this.service.tabs[this.service.selectedTab].files.length; i++) {
|
||||
if (this.service.tabs[this.service.selectedTab].files[i].selected) {
|
||||
file = this.service.tabs[this.service.selectedTab].files[i].path;
|
||||
this.openWithTextEditor(file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
navigateTo(file: string) {
|
||||
this.loading = true;
|
||||
console.log("Navigating to: " + file);
|
||||
this.service.listFiles(file).subscribe((data: any) => {
|
||||
//console.log("files: " + JSON.stringify(data));
|
||||
this.service.tabs[this.service.selectedTab] = {
|
||||
files: data.files,
|
||||
currentDirectory: data.folder,
|
||||
selected: false,
|
||||
folderName: data.folderName,
|
||||
posix: data.posix
|
||||
};
|
||||
this.loading = false;
|
||||
}, err => {
|
||||
this.showError("Unable to list files");
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
navigateHome() {
|
||||
this.loading = true;
|
||||
this.service.listHome().subscribe((data: any) => {
|
||||
this.service.tabs[this.service.selectedTab] = {
|
||||
files: data.files,
|
||||
currentDirectory: data.folder,
|
||||
selected: false,
|
||||
folderName: data.folderName,
|
||||
posix: data.posix
|
||||
};
|
||||
this.loading = false;
|
||||
}, err => {
|
||||
this.showError("Unable to list files");
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
navigateUp(path: string) {
|
||||
this.loading = true;
|
||||
this.service.goUp(this.service.tabs[this.service.selectedTab].currentDirectory).subscribe((data: any) => {
|
||||
this.service.tabs[this.service.selectedTab] = {
|
||||
files: data.files,
|
||||
currentDirectory: data.folder,
|
||||
selected: false,
|
||||
folderName: data.folderName,
|
||||
posix: data.posix
|
||||
};
|
||||
this.loading = false;
|
||||
}, err => {
|
||||
this.showError("Unable to list files");
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
downloadFiles() {
|
||||
let files: string[] = [];
|
||||
for (let i = 0; i < this.service.tabs[this.service.selectedTab].files.length; i++) {
|
||||
if (this.service.tabs[this.service.selectedTab].files[i].selected) {
|
||||
files.push(this.service.tabs[this.service.selectedTab].files[i].name);
|
||||
}
|
||||
}
|
||||
if (files.length < 1) {
|
||||
return;
|
||||
}
|
||||
console.log("To download - files: " + files.join("/"));
|
||||
this.loading = true;
|
||||
this.service.getTempToken().subscribe((data: any) => {
|
||||
let tempToken = data.token;
|
||||
this.loading = false;
|
||||
this.service.downloadFiles(this.service.tabs[this.service.selectedTab].currentDirectory, files.join("/"), tempToken);
|
||||
}, err => {
|
||||
console.log(err);
|
||||
this.showError("Unable to download files");
|
||||
this.loading = false;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
selectAll(val: boolean) {
|
||||
console.log("Selected value: " + val)
|
||||
if (this.service.tabs[this.service.selectedTab].files) {
|
||||
for (let i = 0; i < this.service.tabs[this.service.selectedTab].files.length; i++) {
|
||||
this.service.tabs[this.service.selectedTab].files[i].selected = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileSelectedForUpload(event: any) {
|
||||
console.log("Uploading files: " + JSON.stringify(event.target.files));
|
||||
if (event.target.files && event.target.files.length) {
|
||||
this.service.uploadItem(event.target.files[0], this.service.tabs[this.service.selectedTab].currentDirectory, event.target.files[0].name);
|
||||
}
|
||||
}
|
||||
|
||||
folderSelectedForUpload(event: any) {
|
||||
console.log(JSON.stringify(event.target.files));
|
||||
if (event.target.files && event.target.files.length) {
|
||||
let fileNames: string[] = [];
|
||||
let name: string = event.target.files[0].webkitRelativePath.split("/")[0];
|
||||
|
||||
for (let i = 0; i < event.target.files.length; i++) {
|
||||
let f: any = event.target.files[i];
|
||||
console.log(f.webkitRelativePath);
|
||||
fileNames.push(f.webkitRelativePath);
|
||||
}
|
||||
this.service.uploadFolder(fileNames, event.target.files, this.service.tabs[this.service.selectedTab].currentDirectory, name);
|
||||
}
|
||||
}
|
||||
|
||||
itemClicked(file: FileItem, ctrl: boolean, cls: string) {
|
||||
console.log("width: " + window.innerWidth);
|
||||
if (window.innerWidth < 800) {
|
||||
if (this.multipleSelectionMode) {
|
||||
file.selected = !file.selected;
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.type === 'Directory') {
|
||||
this.navigateTo(file.path);
|
||||
}
|
||||
else {
|
||||
this.openItem(file);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctrl) {
|
||||
file.selected = !file.selected;
|
||||
}
|
||||
else {
|
||||
for (let fileItem of this.service.tabs[this.service.selectedTab].files) {
|
||||
fileItem.selected = false;
|
||||
}
|
||||
file.selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
onKeyDown(event: any) {
|
||||
console.log("onKeyDown: " + event.ctrlKey + " " + event.key)
|
||||
if (event.ctrlKey && event.key == "a") {
|
||||
for (let fileItem of this.service.tabs[this.service.selectedTab].files) {
|
||||
fileItem.selected = true;
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preventTextSelection(event: any) {
|
||||
if (event.detail > 1) {
|
||||
event.preventDefault();
|
||||
// of course, you still do not know what you prevent here...
|
||||
// You could also check event.ctrlKey/event.shiftKey/event.altKey
|
||||
// to not prevent something useful.
|
||||
}
|
||||
}
|
||||
|
||||
showTab(i: number) {
|
||||
console.log("loading folder: " + this.service.tabs[i].currentDirectory);
|
||||
this.service.selectedTab = i;
|
||||
if (!this.service.tabs[i].files) {
|
||||
this.navigateTo(this.service.tabs[i].currentDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
openNewTab(tab: FolderTab) {
|
||||
console.log("files: opening new tab");
|
||||
let n = this.service.tabs.length;
|
||||
this.service.tabs.push(tab);
|
||||
console.log("showing new tab at index: " + n);
|
||||
this.showTab(n);
|
||||
}
|
||||
|
||||
onContextMenu(a, b, c, d) {
|
||||
console.log("Context menu");
|
||||
console.log(c);
|
||||
this.onWindowClicked(c);
|
||||
if (!d.selected) {
|
||||
for (let fileItem of this.service.tabs[this.service.selectedTab].files) {
|
||||
fileItem.selected = false;
|
||||
}
|
||||
d.selected = true;
|
||||
}
|
||||
|
||||
c.style.display = 'flex';
|
||||
|
||||
console.log("Ww: " + window.innerWidth + " Wh: " + window.innerHeight + " height: " + c.offsetHeight);
|
||||
|
||||
if (a.pageX > window.innerWidth / 2) {
|
||||
c.style.left = (a.pageX - c.offsetWidth) + "px";
|
||||
} else {
|
||||
c.style.left = a.pageX + "px";
|
||||
}
|
||||
|
||||
if (a.pageY > window.innerHeight / 2) {
|
||||
c.style.top = (a.pageY - c.offsetHeight) + "px";
|
||||
} else {
|
||||
c.style.top = a.pageY + "px";
|
||||
}
|
||||
|
||||
// c.style.left = a.pageX + "px";
|
||||
// c.style.top = a.pageY + "px";
|
||||
|
||||
// c.style.left = (a.pageX - c.offsetWidth) + "px";
|
||||
// c.style.top = (a.pageY - c.offsetHeight) + "px";
|
||||
|
||||
a.preventDefault();
|
||||
}
|
||||
|
||||
showContextMenu(a, b, c, d, e) {
|
||||
console.log("Context menu");
|
||||
console.log(c);
|
||||
if (!d.selected) {
|
||||
for (let fileItem of this.service.tabs[this.service.selectedTab].files) {
|
||||
fileItem.selected = false;
|
||||
}
|
||||
d.selected = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
c.style.display = 'flex';
|
||||
|
||||
c.style.left = "400px";
|
||||
c.style.top = (a.offsetTop + a.offsetHeight) + "px";
|
||||
|
||||
console.log("Top: " + a.offsetTop + " " + (a.offsetTop + a.offsetHeight));
|
||||
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
onResize(event: any) {
|
||||
let content: HTMLElement = this.list.nativeElement as HTMLElement;
|
||||
let h: HTMLElement = this.header.nativeElement as HTMLElement;
|
||||
console.log("window resized " + content.clientWidth);
|
||||
h.style.width = content.clientWidth + "px";
|
||||
console.log("Header width: " + h.style.width);
|
||||
}
|
||||
|
||||
onWindowClicked(c) {
|
||||
console.log("window clicked");
|
||||
c.style.display = 'none';
|
||||
this.uploadPopup = false;
|
||||
this.showDropdownCtx = false;
|
||||
this.service.sharedMenuListener.next();
|
||||
}
|
||||
|
||||
copyFiles(cut: boolean) {
|
||||
let list: string[] = [];
|
||||
for (let fileItem of this.service.tabs[this.service.selectedTab].files) {
|
||||
if (fileItem.selected) {
|
||||
list.push(fileItem.path);
|
||||
}
|
||||
}
|
||||
this.service.copiedFilePath.files = list;
|
||||
this.service.copiedFilePath.cut = cut;
|
||||
}
|
||||
|
||||
pasteFiles() {
|
||||
console.log("Files to paste: " + JSON.stringify(this.service.copiedFilePath.files));
|
||||
this.service.initMoveOrCopy(this.service.tabs[this.service.selectedTab].currentDirectory, this.service.copiedFilePath.files, this.service.copiedFilePath.cut);
|
||||
}
|
||||
|
||||
closeTab(i: number) {
|
||||
this.service.tabs.splice(i, 1);
|
||||
if (this.service.selectedTab >= this.service.tabs.length) {
|
||||
this.showTab(this.service.tabs.length - 1);
|
||||
} else {
|
||||
this.showTab(this.service.selectedTab);
|
||||
}
|
||||
}
|
||||
|
||||
openInTab() {
|
||||
for (let i = 0; i < this.service.tabs[this.service.selectedTab].files.length; i++) {
|
||||
if (this.service.tabs[this.service.selectedTab].files[i].selected) {
|
||||
let path = this.service.tabs[this.service.selectedTab].files[i].path;
|
||||
this.navigateTo(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
openInNewTab() {
|
||||
for (let i = 0; i < this.service.tabs[this.service.selectedTab].files.length; i++) {
|
||||
if (this.service.tabs[this.service.selectedTab].files[i].selected) {
|
||||
let path = this.service.tabs[this.service.selectedTab].files[i].path;
|
||||
let name = this.service.tabs[this.service.selectedTab].files[i].name;
|
||||
let tab: FolderTab = new FolderTab();
|
||||
tab.currentDirectory = path;
|
||||
tab.folderName = name;
|
||||
tab.files = null;
|
||||
this.service.newTabListener.next(tab);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isSingleSelection(): boolean {
|
||||
if (this.service.tabs && this.service.tabs[this.service.selectedTab] && this.service.tabs[this.service.selectedTab].files) {
|
||||
let c: number = 0;
|
||||
for (let i = 0; i < this.service.tabs[this.service.selectedTab].files.length; i++) {
|
||||
if (this.service.tabs[this.service.selectedTab].files[i].selected) {
|
||||
if (c > 0) {
|
||||
return false;
|
||||
}
|
||||
c++;
|
||||
}
|
||||
}
|
||||
if (c == 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
isAnySelection(): boolean {
|
||||
if (this.service.tabs && this.service.tabs[this.service.selectedTab] && this.service.tabs[this.service.selectedTab].files) {
|
||||
let c: number = 0;
|
||||
for (let i = 0; i < this.service.tabs[this.service.selectedTab].files.length; i++) {
|
||||
if (this.service.tabs[this.service.selectedTab].files[i].selected) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
renameFile(newName: string) {
|
||||
if (newName && newName.length > 0) {
|
||||
for (let i = 0; i < this.service.tabs[this.service.selectedTab].files.length; i++) {
|
||||
if (this.service.tabs[this.service.selectedTab].files[i].selected) {
|
||||
let name = this.service.tabs[this.service.selectedTab].files[i].name;
|
||||
this.loading = true;
|
||||
console.log("Renaming: new-" + newName)
|
||||
this.loading = true;
|
||||
this.service.renameFile(newName, name, this.service.tabs[this.service.selectedTab].currentDirectory).subscribe((rest: any) => {
|
||||
this.loading = false;
|
||||
this.navigateTo(this.service.tabs[this.service.selectedTab].currentDirectory);
|
||||
}, err => {
|
||||
this.showError("Unable to rename files");
|
||||
this.loading = false;
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deleteFiles() {
|
||||
let items: string[] = [];
|
||||
for (let i = 0; i < this.service.tabs[this.service.selectedTab].files.length; i++) {
|
||||
if (this.service.tabs[this.service.selectedTab].files[i].selected) {
|
||||
items.push(this.service.tabs[this.service.selectedTab].files[i].path);
|
||||
}
|
||||
}
|
||||
this.loading = true;
|
||||
if(!confirm("Are you sure, you want to delete?")){
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
this.service.deleteFiles(items).subscribe((rest: any) => {
|
||||
this.loading = false;
|
||||
this.navigateTo(this.service.tabs[this.service.selectedTab].currentDirectory);
|
||||
}, err => {
|
||||
this.showError("Unable to delete files");
|
||||
this.loading = false;
|
||||
})
|
||||
}
|
||||
|
||||
showInfo() {
|
||||
console.log("show info")
|
||||
this.showInfoDialog = true;
|
||||
}
|
||||
|
||||
formatFileSize(size: number) {
|
||||
return utility.formatSize(size);
|
||||
}
|
||||
|
||||
getSelectedFile(): FileItem {
|
||||
for (let i = 0; i < this.service.tabs[this.service.selectedTab].files.length; i++) {
|
||||
if (this.service.tabs[this.service.selectedTab].files[i].selected) {
|
||||
return this.service.tabs[this.service.selectedTab].files[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getSelectedIndex(): number {
|
||||
for (let i = 0; i < this.service.tabs[this.service.selectedTab].files.length; i++) {
|
||||
if (this.service.tabs[this.service.selectedTab].files[i].selected) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
keyup(e: KeyboardEvent) {
|
||||
if (e.keyCode == 40 || e.keyCode == 38) {
|
||||
let index = this.getSelectedIndex();
|
||||
if (index < 0) return;
|
||||
let container = this.list;
|
||||
let children = container.nativeElement.children;
|
||||
if (this.service.tabs[this.service.selectedTab].files.length - 1 == index || (e.keyCode == 38 && index == 0)) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < this.service.tabs[this.service.selectedTab].files.length; i++) {
|
||||
this.service.tabs[this.service.selectedTab].files[i].selected = false;
|
||||
}
|
||||
this.service.tabs[this.service.selectedTab].files[e.keyCode == 40 ? index + 1 : index - 1].selected = true;
|
||||
let item: HTMLElement = <HTMLElement>children.item(e.keyCode == 40 ? index + 1 : index - 1);
|
||||
item.focus();
|
||||
}
|
||||
}
|
||||
|
||||
sort(col) {
|
||||
this.sortColumn = col;
|
||||
// if (this.ascendingSort == undefined) {
|
||||
// this.ascendingSort = true;
|
||||
// } else {
|
||||
// this.ascendingSort = !this.ascendingSort;
|
||||
// }
|
||||
|
||||
this.ascendingSort = !this.ascendingSort;
|
||||
|
||||
console.log("this.sortColumn: " + this.sortColumn + " this.ascendingSort: " + this.ascendingSort);
|
||||
switch (this.sortColumn) {
|
||||
case 0: {
|
||||
this.service.tabs[this.service.selectedTab].files.sort((a: FileItem, b: FileItem) => {
|
||||
if (a.type === "Directory" && b.type === "Directory") {
|
||||
return this.ascendingSort ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
|
||||
} else if (a.type === "Directory") {
|
||||
return this.ascendingSort ? 1 : -1;
|
||||
} else if (b.type === "Directory") {
|
||||
return this.ascendingSort ? -1 : 1;
|
||||
} else {
|
||||
return this.ascendingSort ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
this.service.tabs[this.service.selectedTab].files.sort((a: FileItem, b: FileItem) => {
|
||||
if (a.type === "Directory" && b.type === "Directory") {
|
||||
return 0;
|
||||
} else if (a.type === "Directory") {
|
||||
return this.ascendingSort ? 1 : -1;
|
||||
} else if (b.type === "Directory") {
|
||||
return this.ascendingSort ? -1 : 1;
|
||||
} else {
|
||||
return this.ascendingSort ? a.size - b.size : b.size - a.size;
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
this.service.tabs[this.service.selectedTab].files.sort((a: FileItem, b: FileItem) => {
|
||||
if (a.type === "Directory" && b.type === "Directory") {
|
||||
if (this.ascendingSort) {
|
||||
return a.lastModified > b.lastModified ? 1 : -1;
|
||||
} else {
|
||||
return a.lastModified > b.lastModified ? -1 : 1;
|
||||
}
|
||||
} else if (a.type === "Directory") {
|
||||
return this.ascendingSort ? 1 : -1;
|
||||
} else if (b.type === "Directory") {
|
||||
return this.ascendingSort ? -1 : 1;
|
||||
} else {
|
||||
if (this.ascendingSort) {
|
||||
return a.lastModified > b.lastModified ? 1 : -1;
|
||||
} else {
|
||||
return a.lastModified > b.lastModified ? -1 : 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
this.service.tabs[this.service.selectedTab].files.sort((a: FileItem, b: FileItem) => {
|
||||
if (a.type === "Directory" && b.type === "Directory") {
|
||||
if (this.ascendingSort) {
|
||||
return a.permissionString.localeCompare(b.permissionString);
|
||||
} else {
|
||||
return b.permissionString.localeCompare(a.permissionString);
|
||||
}
|
||||
} else if (a.type === "Directory") {
|
||||
return this.ascendingSort ? 1 : -1;
|
||||
} else if (b.type === "Directory") {
|
||||
return this.ascendingSort ? -1 : 1;
|
||||
} else {
|
||||
if (this.ascendingSort) {
|
||||
return a.permissionString.localeCompare(b.permissionString);
|
||||
} else {
|
||||
return b.permissionString.localeCompare(a.permissionString);
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
this.service.tabs[this.service.selectedTab].files.sort((a: FileItem, b: FileItem) => {
|
||||
if (a.type === "Directory" && b.type === "Directory") {
|
||||
if (this.ascendingSort) {
|
||||
return a.user.localeCompare(b.user);
|
||||
} else {
|
||||
return b.user.localeCompare(a.user);
|
||||
}
|
||||
} else if (a.type === "Directory") {
|
||||
return this.ascendingSort ? 1 : -1;
|
||||
} else if (b.type === "Directory") {
|
||||
return this.ascendingSort ? -1 : 1;
|
||||
} else {
|
||||
if (this.ascendingSort) {
|
||||
return a.user.localeCompare(b.user);
|
||||
} else {
|
||||
return b.user.localeCompare(a.user);
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showDropDownMenu(contextmenu: any, $event: any) {
|
||||
this.onWindowClicked(contextmenu);
|
||||
this.showDropdownCtx = !this.showDropdownCtx;
|
||||
$event.stopPropagation();
|
||||
}
|
||||
|
||||
createFolder($event: any) {
|
||||
console.log($event);
|
||||
this.loading = true;
|
||||
this.service.mkdir(this.service.tabs[this.service.selectedTab].currentDirectory, $event).subscribe((resp: any) => {
|
||||
console.log("success");
|
||||
this.navigateTo(this.service.tabs[this.service.selectedTab].currentDirectory);
|
||||
}, err => {
|
||||
this.showError("Unable to create folder");
|
||||
this.loading = false;
|
||||
})
|
||||
}
|
||||
|
||||
createFile($event: any) {
|
||||
console.log($event);
|
||||
this.loading = true;
|
||||
this.service.touch(this.service.tabs[this.service.selectedTab].currentDirectory, $event).subscribe((resp: any) => {
|
||||
console.log("success");
|
||||
this.navigateTo(this.service.tabs[this.service.selectedTab].currentDirectory);
|
||||
}, err => {
|
||||
this.showError("Unable to create file");
|
||||
this.loading = false;
|
||||
})
|
||||
}
|
||||
|
||||
newFolder() {
|
||||
this.showNewFolderItem = true;
|
||||
}
|
||||
|
||||
newFile() {
|
||||
this.showNewFileItem = true;
|
||||
}
|
||||
|
||||
showError(msg: string) {
|
||||
this.toastMessage = msg;
|
||||
this.toastVisible = true;
|
||||
}
|
||||
|
||||
getFileIcon(file: FileItem): string {
|
||||
if ((file.type == 'Directory' || file.type == 'DirLink')) {
|
||||
return 'fa fa-folder';
|
||||
}
|
||||
else if (file.type.startsWith("image")) {
|
||||
return 'fa fa-file-image-o';
|
||||
}
|
||||
else if (file.type.startsWith("video")) {
|
||||
return 'fa fa-file-movie-o';
|
||||
}
|
||||
else if (file.type.startsWith("audio")) {
|
||||
return 'fa fa-file-audio-o';
|
||||
}
|
||||
else if (file.type.startsWith("text/")) {
|
||||
return 'fa fa-file-text-o';
|
||||
}
|
||||
else if (file.type.includes("application/zip") ||
|
||||
file.type.includes("application/x-freearc") ||
|
||||
file.type.includes("application/x-bzip") ||
|
||||
file.type.includes("application/java-archive") ||
|
||||
file.type.includes("application/x-rar-compressed") ||
|
||||
file.type.includes("application/x-tar") ||
|
||||
file.type.includes("application/x-7z-compressed") ||
|
||||
file.type.includes("application/x-gzip") ||
|
||||
file.type.includes("application/gzip") ||
|
||||
file.type.includes("application/vnd.android.package-archive") ||
|
||||
file.type.includes("application/x-bcpio") ||
|
||||
file.type.includes("application/x-cpio") ||
|
||||
file.type.includes("application/x-stuffit") ||
|
||||
file.type.includes("application/x-xz") ||
|
||||
file.type.includes("application/x-debian-package") ||
|
||||
file.type.includes("application/x-redhat-package-manager")) {
|
||||
return 'fa fa-file-archive-o';
|
||||
}
|
||||
else if (file.type.startsWith("application/pdf")) {
|
||||
return 'fa fa-file-pdf-o';
|
||||
}
|
||||
else if (file.type.startsWith("application/javascript") ||
|
||||
file.type.includes("application/json")||
|
||||
file.type.includes("application/xml")) {
|
||||
return 'fa fa-file-text-o';
|
||||
}
|
||||
else if (file.type.includes("application/vnd.openxmlformats-officedocument") ||
|
||||
file.type.includes("application/msword")) {
|
||||
return 'fa fa-file-text-o';
|
||||
}
|
||||
else {
|
||||
return 'fa fa-file-o';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<div style="width: 100%; min-width: 200px; padding: 5px; display: flex; padding-left: 10px;" *ngIf="model">
|
||||
<span style="padding-left: 5px; width: 20px; min-width: 20px; text-align: center; cursor: pointer;" (click)="toggleState()">
|
||||
<i *ngIf="!model.leafNode" [class]="model.expanded?'fa fa-angle-down': 'fa fa-angle-right'" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span *ngIf="model.leafNode" style="padding-left: 5px;">
|
||||
<i class="fa fa-file-o" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span *ngIf="!model.leafNode" style="padding-left: 5px;">
|
||||
<i [class]="icon?('fa '+icon):'fa fa-folder-o'" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span style="padding-left: 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer;"
|
||||
class="noselect" (click)="onClick()" (contextmenu)="onContextMenu($event,contextmenu)">{{model.name}}</span>
|
||||
</div>
|
||||
<div *ngIf="model&&!model.leafNode&&model.children&&model.expanded&&model.children.length>0"
|
||||
style="padding-left: 15px;">
|
||||
<app-tree style="flex: 1" *ngFor="let node of model.children" [model]="node" [parent]="model"></app-tree>
|
||||
</div>
|
||||
|
||||
|
||||
<div [style.display]="showCtx?'flex':'none'"
|
||||
style="z-index: 99; width: 200px; flex-direction: column;display: none; position: fixed; background: white; border: 1px solid rgb(200,200,200); box-shadow: 3px 3px 10px 0px rgb(200,200,200);"
|
||||
#contextmenu>
|
||||
<div class="context-menu-items" (click)="openInNewTab()">Open in new tab</div>
|
||||
<div class="context-menu-items" (click)="openInTab()">Open</div>
|
||||
<div class="context-menu-items" (click)="copyPath()">Copy path</div>
|
||||
</div>
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TreeComponent } from './tree.component';
|
||||
|
||||
describe('TreeComponent', () => {
|
||||
let component: TreeComponent;
|
||||
let fixture: ComponentFixture<TreeComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ TreeComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TreeComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,144 @@
|
|||
import { Component, OnInit, Input, OnChanges, ViewChild, ElementRef, OnDestroy, Output, EventEmitter } from '@angular/core';
|
||||
import { DataService } from 'src/app/data.service';
|
||||
import { NavigationTreeNode } from 'src/app/model/navigation-tree-node';
|
||||
import { Subscriber, Subscription } from 'rxjs';
|
||||
import { FolderTab } from 'src/app/model/folder-tab';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tree',
|
||||
templateUrl: './tree.component.html',
|
||||
styleUrls: ['./tree.component.css']
|
||||
})
|
||||
export class TreeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
@Input()
|
||||
model: NavigationTreeNode;
|
||||
|
||||
@Input()
|
||||
icon: string;
|
||||
|
||||
@Input()
|
||||
parent: NavigationTreeNode;
|
||||
|
||||
showCtx: boolean = false;
|
||||
|
||||
menuEventSubsciption: Subscription;
|
||||
|
||||
constructor(public service: DataService) { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
if (this.model && this.model.expanded) {
|
||||
this.loadChildren();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.menuEventSubsciption) {
|
||||
this.menuEventSubsciption.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
toggleState() {
|
||||
if (!this.model) {
|
||||
return;
|
||||
}
|
||||
if (this.model.leafNode) {
|
||||
return;
|
||||
}
|
||||
console.log("Togging state")
|
||||
this.model.expanded = !this.model.expanded;
|
||||
if (this.model.expanded) {
|
||||
this.loadChildren();
|
||||
}
|
||||
}
|
||||
|
||||
loadChildren() {
|
||||
if (!this.model.children) {
|
||||
if (!this.parent && this.model.name == "Home") {
|
||||
console.log("loading children for: " + this.model.name);
|
||||
this.service.getHomeChildren().subscribe((data: NavigationTreeNode[]) => {
|
||||
//console.log("loading children for: " + JSON.stringify(data));
|
||||
this.model.children = data;
|
||||
});
|
||||
}
|
||||
else if (this.parent) {
|
||||
console.log("loading children for: " + this.model.name);
|
||||
this.service.getDirectoryChildren(this.model.path).subscribe((data: NavigationTreeNode[]) => {
|
||||
//console.log("loading children for: " + JSON.stringify(data));
|
||||
this.model.children = data;
|
||||
});
|
||||
}
|
||||
else if (!this.parent && this.model.name == "File system") {
|
||||
console.log("loading children for: " + this.model.name);
|
||||
this.service.getFsChildren().subscribe((data: NavigationTreeNode[]) => {
|
||||
// console.log("loading children for: " + JSON.stringify(data));
|
||||
this.model.children = data;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClick() {
|
||||
console.log("tree clicked: " + this.model.path);
|
||||
this.openInTab();
|
||||
}
|
||||
|
||||
onContextMenu(a: any, c: any) {
|
||||
if (this.model.leafNode) {
|
||||
a.preventDefault();
|
||||
return;
|
||||
}
|
||||
this.showCtx = true;
|
||||
c.style.display = 'flex';
|
||||
|
||||
console.log("Ww: " + window.innerWidth + " Wh: " + window.innerHeight + " height: " + c.offsetHeight);
|
||||
|
||||
if (a.pageX > window.innerWidth / 2) {
|
||||
c.style.left = (a.pageX - c.offsetWidth) + "px";
|
||||
} else {
|
||||
c.style.left = a.pageX + "px";
|
||||
}
|
||||
|
||||
if (a.pageY > window.innerHeight / 2) {
|
||||
c.style.top = (a.pageY - c.offsetHeight) + "px";
|
||||
} else {
|
||||
c.style.top = a.pageY + "px";
|
||||
}
|
||||
|
||||
this.service.sharedMenuListener.next();
|
||||
this.menuEventSubsciption = this.service.sharedMenuListener.subscribe((event) => {
|
||||
this.hideContextMenu();
|
||||
this.menuEventSubsciption.unsubscribe();
|
||||
this.menuEventSubsciption = null;
|
||||
})
|
||||
console.log("context menu on tree clicked: " + this.model.path);
|
||||
a.preventDefault();
|
||||
}
|
||||
|
||||
hideContextMenu() {
|
||||
this.showCtx = false;
|
||||
console.log("Hiding context menu")
|
||||
}
|
||||
|
||||
openInNewTab() {
|
||||
console.log("openInNewTab " + this.model.path);
|
||||
let tab: FolderTab = new FolderTab();
|
||||
tab.currentDirectory = this.model.path;
|
||||
tab.folderName = this.model.name;
|
||||
tab.files = null;
|
||||
this.service.newTabListener.next(tab);
|
||||
}
|
||||
|
||||
openInTab() {
|
||||
console.log("openInTab " + this.model.path);
|
||||
this.service.currentTabListener.next({ path: this.model.path, file: this.model.leafNode });
|
||||
}
|
||||
|
||||
copyPath() {
|
||||
console.log("copyPath " + this.model.path);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<div
|
||||
style="position: fixed; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); left: 0px; top: 0px; z-index: 302; display: flex; flex-direction: column; justify-content: center; align-content: center; align-items: center;">
|
||||
<img *ngIf="url" [src]="src" style="margin: auto; display: block; max-height: 90vh;">
|
||||
<div style="position: fixed; top: 0px; right: 0px; padding: 10px; color: white; font-size: 20px; cursor: pointer;" (click)="close()">
|
||||
<span><i class="fa fa-times"></i></span>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ImageViewerComponent } from './image-viewer.component';
|
||||
|
||||
describe('ImageViewerComponent', () => {
|
||||
let component: ImageViewerComponent;
|
||||
let fixture: ComponentFixture<ImageViewerComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ImageViewerComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ImageViewerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
import { Component, OnInit, Input, OnChanges, Output, EventEmitter } from '@angular/core';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { DataService } from 'src/app/data.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-image-viewer',
|
||||
templateUrl: './image-viewer.component.html',
|
||||
styleUrls: ['./image-viewer.component.css']
|
||||
})
|
||||
export class ImageViewerComponent implements OnInit, OnChanges {
|
||||
|
||||
@Input()
|
||||
url: string;
|
||||
src: string;
|
||||
|
||||
@Output()
|
||||
componentClosed = new EventEmitter<any>();
|
||||
|
||||
constructor(private service: DataService) { }
|
||||
|
||||
ngOnInit() {
|
||||
console.log("image viewer init");
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
if (this.url) {
|
||||
this.src = environment.BIN_URL + "image/" + btoa(this.url) + "?token=" + this.service.getJwtToken();
|
||||
console.log("image src " + this.src);
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.componentClosed.emit({});
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue