initial commit

master
subhra74 2019-07-02 21:30:57 +02:00
commit dde98003b0
170 changed files with 23243 additions and 0 deletions

44
.gitignore vendored Normal file
View File

@ -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

15
app/HELP.md Normal file
View File

@ -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/)

286
app/mvnw vendored Normal file
View File

@ -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 "$@"

161
app/mvnw.cmd vendored Normal file
View File

@ -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%

151
app/pom.xml Normal file
View File

@ -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>

View File

@ -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<>();
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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";
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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");
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View 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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}
}

View File

@ -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");
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 434 KiB

View File

@ -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

View File

@ -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

View File

@ -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"));
}
}

13
ui/.editorconfig Normal file
View File

@ -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

44
ui/.gitignore vendored Normal file
View File

@ -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

27
ui/README.md Normal file
View File

@ -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).

135
ui/angular.json Normal file
View File

@ -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"
}

28
ui/e2e/protractor.conf.js Normal file
View File

@ -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 } }));
}
};

View File

@ -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,
}));
});
});

11
ui/e2e/src/app.po.ts Normal file
View File

@ -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();
}
}

13
ui/e2e/tsconfig.e2e.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

10857
ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

53
ui/package.json Normal file
View File

@ -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"
}
}

View File

@ -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 { }

View File

View File

@ -0,0 +1 @@
<router-outlet></router-outlet>

View File

@ -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!');
});
});

View File

@ -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';
}

62
ui/src/app/app.module.ts Normal file
View File

@ -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 { }

View File

@ -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();
});
});

400
ui/src/app/data.service.ts Normal file
View File

@ -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");
}
}

View File

@ -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();
}));
});

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -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();
});
});

View File

@ -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() {
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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({});
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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> -->

View File

@ -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();
});
});

View File

@ -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';
}
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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