Initial commit
This commit is contained in:
13
src/main/java/com/github/russp/bpe/BpeApplication.java
Normal file
13
src/main/java/com/github/russp/bpe/BpeApplication.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.github.russp.bpe;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class BpeApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(BpeApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.github.russp.bpe.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
|
||||
|
||||
@EnableWebFluxSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Value("${app.username}")
|
||||
private String username;
|
||||
@Value("${app.password}")
|
||||
private String password;
|
||||
|
||||
@Bean
|
||||
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
|
||||
return http.authorizeExchange()
|
||||
.pathMatchers("/login", "/*.css", "/*.js").permitAll()
|
||||
.anyExchange().authenticated()
|
||||
.and().formLogin().loginPage("/login")
|
||||
.and().logout()
|
||||
.requiresLogout(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/logout"))
|
||||
.and()
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MapReactiveUserDetailsService userDetailsService() {
|
||||
UserDetails user = User
|
||||
.withUsername(username)
|
||||
.password(passwordEncoder().encode(password))
|
||||
.roles("USER")
|
||||
.build();
|
||||
password = "";
|
||||
return new MapReactiveUserDetailsService(user);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.github.russp.bpe.http;
|
||||
|
||||
import com.github.russp.bpe.service.FilesService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.multipart.FilePart;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.ArrayList;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class BrowserController {
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM);
|
||||
|
||||
private final FilesService filesService;
|
||||
|
||||
@GetMapping("/")
|
||||
public String list(@RequestParam(defaultValue = "/") String path, Model model) {
|
||||
log.info("Path: {}", path);
|
||||
|
||||
var files = filesService.ls(path)
|
||||
.map(file -> FileDto.builder()
|
||||
.dir(file.isDir())
|
||||
.name(file.getName())
|
||||
.fullName(file.getFullName())
|
||||
.size(String.valueOf(file.getSize()))
|
||||
.modifiedAt(file.getModifiedAt().format(DATE_TIME_FORMATTER))
|
||||
.build())
|
||||
.collectList();
|
||||
|
||||
var breadcrumbs = filesService.pwd(path).map(file -> FileDto.builder()
|
||||
.name(file.getName())
|
||||
.fullName(file.getFullName())
|
||||
.build())
|
||||
.collect(
|
||||
ArrayList::new,
|
||||
(list, value) -> list.add(0, value)
|
||||
);
|
||||
|
||||
model.addAttribute("files", files);
|
||||
model.addAttribute("breadcrumbs", breadcrumbs);
|
||||
|
||||
return "browse";
|
||||
}
|
||||
|
||||
@GetMapping("edit")
|
||||
public String edit(@RequestParam String path, Model model) {
|
||||
var breadcrumbs = filesService.parent(path).map(file -> FileDto.builder()
|
||||
.name(file.getName())
|
||||
.fullName(file.getFullName())
|
||||
.build())
|
||||
.collect(
|
||||
ArrayList::new,
|
||||
(list, value) -> list.add(0, value)
|
||||
);
|
||||
model.addAttribute("breadcrumbs", breadcrumbs);
|
||||
model.addAttribute("selectedFile", path);
|
||||
|
||||
model.addAttribute("selectedFileName", Paths.get(path).getFileName());
|
||||
return "edit";
|
||||
}
|
||||
|
||||
@GetMapping(value = "/content",
|
||||
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||
public @ResponseBody Flux<DataBuffer> content(String path) {
|
||||
return filesService.content(path);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/content")
|
||||
public @ResponseBody Mono<Void> save(@RequestParam String path,
|
||||
@RequestPart("file") Mono<FilePart> filePartMono) {
|
||||
return filePartMono
|
||||
.flatMap(fp -> filesService.save(path, fp::transferTo))
|
||||
.then(Mono.empty());
|
||||
}
|
||||
}
|
||||
21
src/main/java/com/github/russp/bpe/http/FileDto.java
Normal file
21
src/main/java/com/github/russp/bpe/http/FileDto.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.github.russp.bpe.http;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class FileDto {
|
||||
private String name;
|
||||
private String size;
|
||||
private String modifiedAt;
|
||||
private boolean dir;
|
||||
|
||||
private String fullName;
|
||||
}
|
||||
16
src/main/java/com/github/russp/bpe/http/LoginController.java
Normal file
16
src/main/java/com/github/russp/bpe/http/LoginController.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.github.russp.bpe.http;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/login")
|
||||
public class LoginController {
|
||||
|
||||
@GetMapping
|
||||
public String login() {
|
||||
return "login";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.github.russp.bpe.http;
|
||||
|
||||
import org.springframework.security.web.reactive.result.view.CsrfRequestDataValueProcessor;
|
||||
import org.springframework.security.web.server.csrf.CsrfToken;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@ControllerAdvice
|
||||
public class SecurityControllerAdvice {
|
||||
|
||||
@ModelAttribute(CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME)
|
||||
Mono<CsrfToken> csrfToken(ServerWebExchange exchange) {
|
||||
return exchange.getAttribute(CsrfToken.class.getName());
|
||||
}
|
||||
|
||||
}
|
||||
27
src/main/java/com/github/russp/bpe/model/FileEntry.java
Normal file
27
src/main/java/com/github/russp/bpe/model/FileEntry.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.github.russp.bpe.model;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public class FileEntry implements Comparable<FileEntry> {
|
||||
private final boolean dir;
|
||||
private final String name;
|
||||
private final String fullName;
|
||||
private final long size;
|
||||
private final LocalDateTime modifiedAt;
|
||||
|
||||
@Override
|
||||
public int compareTo(FileEntry o) {
|
||||
if (dir && !o.isDir()) {
|
||||
return -1;
|
||||
}
|
||||
if (!dir && o.isDir()) {
|
||||
return 1;
|
||||
}
|
||||
return name.compareTo(o.getName());
|
||||
}
|
||||
}
|
||||
126
src/main/java/com/github/russp/bpe/service/FilesService.java
Normal file
126
src/main/java/com/github/russp/bpe/service/FilesService.java
Normal file
@@ -0,0 +1,126 @@
|
||||
package com.github.russp.bpe.service;
|
||||
|
||||
import com.github.russp.bpe.model.FileEntry;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
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.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.function.Function;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FilesService {
|
||||
|
||||
@Value("${app.fs.root}")
|
||||
private String root;
|
||||
|
||||
public Flux<FileEntry> ls(String path) {
|
||||
try {
|
||||
var rootPath = Paths.get(root);
|
||||
return Flux.fromStream(Files.list(Path.of(root, path))
|
||||
.map(p -> FileEntry.builder()
|
||||
.name(p.getFileName().toString())
|
||||
.fullName(p.toString().replace(root, ""))
|
||||
.dir(Files.isDirectory(p))
|
||||
.modifiedAt(getLastModifiedTime(p))
|
||||
.size(size(p))
|
||||
.build()
|
||||
)
|
||||
.sorted()
|
||||
);
|
||||
} catch (IOException e) {
|
||||
log.debug("Listing error for path {}: {}", path, e.getMessage());
|
||||
return Flux.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Flux<FileEntry> parent(String path) {
|
||||
var parent = Path.of(path).getParent();
|
||||
if (parent == null) {
|
||||
return Flux.empty();
|
||||
}
|
||||
return pwd(Path.of(path).getParent().toString());
|
||||
}
|
||||
|
||||
public Flux<FileEntry> pwd(String path) {
|
||||
if (!StringUtils.hasText(path)) {
|
||||
return Flux.empty();
|
||||
}
|
||||
var rootPath = Path.of(root);
|
||||
var end = Path.of(root, path);
|
||||
|
||||
return Flux.<Path, Path>generate(() -> end, (state, sink) -> {
|
||||
if (rootPath.equals(state)) {
|
||||
sink.complete();
|
||||
}
|
||||
sink.next(state);
|
||||
return state.getParent();
|
||||
})
|
||||
.map(p -> FileEntry.builder()
|
||||
.name(p.getFileName().toString())
|
||||
.fullName(p.toString().replace(root, ""))
|
||||
.dir(Files.isDirectory(p))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
public Flux<DataBuffer> content(String path) {
|
||||
var file = Path.of(root, path);
|
||||
return DataBufferUtils.read(file, DefaultDataBufferFactory.sharedInstance, 1024, StandardOpenOption.READ);
|
||||
}
|
||||
|
||||
public Mono<Void> save(String path, Function<Path, Mono<Void>> transferTo) {
|
||||
return Mono.just(Path.of(root, path))
|
||||
.doOnNext(fn -> {
|
||||
var file = fn.toFile();
|
||||
try {
|
||||
FileUtils.moveFile(file, new File(fn.toString() + ".bak"), StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
log.error("Can not create backup file for {}", path, e);
|
||||
// throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.flatMap(transferTo);
|
||||
}
|
||||
|
||||
private static LocalDateTime getLastModifiedTime(Path path) {
|
||||
try {
|
||||
return LocalDateTime.ofInstant(
|
||||
Files.getLastModifiedTime(path).toInstant(),
|
||||
ZoneId.systemDefault());
|
||||
} catch (IOException e) {
|
||||
return LocalDateTime.MIN;
|
||||
}
|
||||
}
|
||||
|
||||
private static long size(Path path) {
|
||||
try {
|
||||
if (Files.isRegularFile(path)) {
|
||||
return Files.size(path);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.debug("Size error for {}: {}", path, e.getMessage());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
13
src/main/resources/application.yml
Normal file
13
src/main/resources/application.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
app:
|
||||
fs:
|
||||
root: ${APP_FS_ROOT:/app/fs}
|
||||
bak:
|
||||
count: 5
|
||||
username: ${APP_USERNAME:admin}
|
||||
password: ${APP_PASSWORD:admin}
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
logging:
|
||||
level:
|
||||
org.springframework.web: DEBUG
|
||||
89
src/main/resources/static/js/minimal-theme-switcher.js
Normal file
89
src/main/resources/static/js/minimal-theme-switcher.js
Normal file
@@ -0,0 +1,89 @@
|
||||
/*!
|
||||
* Minimal theme switcher
|
||||
*
|
||||
* Pico.css - https://picocss.com
|
||||
* Copyright 2019-2022 - Licensed under MIT
|
||||
*/
|
||||
|
||||
const themeSwitcher = {
|
||||
|
||||
// Config
|
||||
_scheme: "auto",
|
||||
menuTarget: "details[role='list']",
|
||||
buttonsTarget: "a[data-theme-switcher]",
|
||||
buttonAttribute: "data-theme-switcher",
|
||||
rootAttribute: "data-theme",
|
||||
localStorageKey: "picoPreferedColorScheme",
|
||||
|
||||
// Init
|
||||
init() {
|
||||
this.scheme = this.schemeFromLocalStorage;
|
||||
this.initSwitchers();
|
||||
},
|
||||
|
||||
// Get color scheme from local storage
|
||||
get schemeFromLocalStorage() {
|
||||
if (typeof window.localStorage !== "undefined") {
|
||||
if (window.localStorage.getItem(this.localStorageKey) !== null) {
|
||||
return window.localStorage.getItem(this.localStorageKey);
|
||||
}
|
||||
}
|
||||
return this._scheme;
|
||||
},
|
||||
|
||||
// Prefered color scheme
|
||||
get preferedColorScheme() {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
},
|
||||
|
||||
// Init switchers
|
||||
initSwitchers() {
|
||||
const buttons = document.querySelectorAll(this.buttonsTarget);
|
||||
buttons.forEach((button) => {
|
||||
button.addEventListener("click", event => {
|
||||
event.preventDefault();
|
||||
// Set scheme
|
||||
this.scheme = button.getAttribute(this.buttonAttribute);
|
||||
// Close dropdown
|
||||
document.querySelector(this.menuTarget).removeAttribute("open");
|
||||
}, false);
|
||||
});
|
||||
},
|
||||
|
||||
// Set scheme
|
||||
set scheme(scheme) {
|
||||
if (scheme == "auto") {
|
||||
this.preferedColorScheme == "dark"
|
||||
? (this._scheme = "dark")
|
||||
: (this._scheme = "light");
|
||||
} else if (scheme == "dark" || scheme == "light") {
|
||||
this._scheme = scheme;
|
||||
}
|
||||
this.applyScheme();
|
||||
this.schemeToLocalStorage();
|
||||
},
|
||||
|
||||
// Get scheme
|
||||
get scheme() {
|
||||
return this._scheme;
|
||||
},
|
||||
|
||||
// Apply scheme
|
||||
applyScheme() {
|
||||
document
|
||||
.querySelector("html")
|
||||
.setAttribute(this.rootAttribute, this.scheme);
|
||||
},
|
||||
|
||||
// Store scheme to local storage
|
||||
schemeToLocalStorage() {
|
||||
if (typeof window.localStorage !== "undefined") {
|
||||
window.localStorage.setItem(this.localStorageKey, this.scheme);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Init
|
||||
themeSwitcher.init();
|
||||
38
src/main/resources/static/stylesheet.css
Normal file
38
src/main/resources/static/stylesheet.css
Normal file
@@ -0,0 +1,38 @@
|
||||
/* Global CSS variables */
|
||||
:root {
|
||||
--spacing: 0.1em;
|
||||
--t-ypography-spacing-vertical: 0px;
|
||||
}
|
||||
|
||||
#editor {
|
||||
font-size: 1.2em;
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 75vh;
|
||||
}
|
||||
|
||||
ul.breadcrumbs > li {
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
ul.breadcrumbs > li:after {
|
||||
content: "/";
|
||||
text-indent: 4px;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
ul.breadcrumbs > li:last-child:after {
|
||||
content: "";
|
||||
}
|
||||
|
||||
/*body > header {*/
|
||||
/* padding-top: 0px;*/
|
||||
/* padding-bottom: 0px;*/
|
||||
/*}*/
|
||||
|
||||
/*body > main {*/
|
||||
/* padding-top: 16px;*/
|
||||
/* padding-bottom: 16px;*/
|
||||
/*}*/
|
||||
99
src/main/resources/templates/browse.mustache
Normal file
99
src/main/resources/templates/browse.mustache
Normal file
@@ -0,0 +1,99 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Browser">
|
||||
|
||||
<title>Index of </title>
|
||||
|
||||
<link rel="shortcut icon" href="https://picocss.com/favicon.ico">
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.4/css/all.css"
|
||||
integrity="sha384-DyZ88mC6Up2uqS4h/KRgHuoeGwBcD4Ng9SiP4dIRy0EXTlnuz47vAwmeGwVChigm" crossorigin="anonymous"/>
|
||||
|
||||
<!-- Pico.css -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.min.css">
|
||||
<!-- Custom template-->
|
||||
<link rel="stylesheet" href="/stylesheet.css">
|
||||
|
||||
<style></style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header class="container">
|
||||
<h5>Browse</h5>
|
||||
<nav>
|
||||
<ul class="breadcrumbs">
|
||||
<li><a href="/"><i class="fa fa-home"></i></a></li>
|
||||
{{#breadcrumbs}}
|
||||
<li><a href="/?path={{fullName}}">{{name}}</a></li>
|
||||
{{/breadcrumbs}}
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Main -->
|
||||
<main class="container">
|
||||
<!-- Tables -->
|
||||
<section id="tables">
|
||||
<figure>
|
||||
<table role="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">Size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#files}}
|
||||
<tr>
|
||||
<th scope="row">
|
||||
{{#dir}}
|
||||
<a href="/?path={{fullName}}">
|
||||
<i class="fa fa-folder"></i>
|
||||
{{name}}
|
||||
</a>
|
||||
{{/dir}}
|
||||
{{^dir}}
|
||||
<a href="/edit?path={{fullName}}">
|
||||
<i class="fa fa-file"></i>
|
||||
{{name}}
|
||||
</a>
|
||||
{{/dir}}
|
||||
</th>
|
||||
<td>{{modifiedAt}}</td>
|
||||
<td>{{size}}</td>
|
||||
</tr>
|
||||
{{/files}}
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
</section><!-- ./ Tables -->
|
||||
|
||||
</main><!-- ./ Main -->
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="container">
|
||||
|
||||
<details role="list">
|
||||
<summary aria-haspopup="listbox" role="button" class="secondary">Theme</summary>
|
||||
<ul role="listbox">
|
||||
<li><a href="#" data-theme-switcher="auto">Auto</a></li>
|
||||
<li><a href="#" data-theme-switcher="light">Light</a></li>
|
||||
<li><a href="#" data-theme-switcher="dark">Dark</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<small>Built with <a href="https://picocss.com">Pico</a> • <a
|
||||
href="https://github.com/picocss/examples/blob/master/basic-template/">Source code</a></small>
|
||||
</footer><!-- ./ Footer -->
|
||||
|
||||
|
||||
<!-- Minimal theme switcher -->
|
||||
<script src="../js/minimal-theme-switcher.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
35
src/main/resources/templates/browse/header.mustache
Normal file
35
src/main/resources/templates/browse/header.mustache
Normal file
@@ -0,0 +1,35 @@
|
||||
<header class="container">
|
||||
<hgroup>
|
||||
<h1>Basic template</h1>
|
||||
<h2>A basic custom template for Pico using only CSS custom properties (variables).</h2>
|
||||
</hgroup>
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<details role="list">
|
||||
<summary aria-haspopup="listbox" role="button" class="secondary">Theme</summary>
|
||||
<ul role="listbox">
|
||||
<li><a href="#" data-theme-switcher="auto">Auto</a></li>
|
||||
<li><a href="#" data-theme-switcher="light">Light</a></li>
|
||||
<li><a href="#" data-theme-switcher="dark">Dark</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li>
|
||||
<details role="list">
|
||||
<summary aria-haspopup="listbox">Examples</summary>
|
||||
<ul role="listbox">
|
||||
<li><a href="../preview/">Preview</a></li>
|
||||
<li><a href="../preview-rtl/">Right-to-left</a></li>
|
||||
<li><a href="../classless/">Class-less</a></li>
|
||||
<li><a href="../basic-template/">Basic template</a></li>
|
||||
<li><a href="../company/">Company</a></li>
|
||||
<li><a href="../google-amp/">Google Amp</a></li>
|
||||
<li><a href="../sign-in/">Sign in</a></li>
|
||||
<li><a href="../bootstrap-grid/">Bootstrap grid</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
0
src/main/resources/templates/browse/list.mustache
Normal file
0
src/main/resources/templates/browse/list.mustache
Normal file
136
src/main/resources/templates/edit.mustache
Normal file
136
src/main/resources/templates/edit.mustache
Normal file
@@ -0,0 +1,136 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Browser">
|
||||
|
||||
<meta name="_csrf" content="{{_csrf.token}}"/>
|
||||
<meta name="_csrf_header" content="{{_csrf.headerName}}"/>
|
||||
|
||||
<title>Edit {{selectedFileName}}</title>
|
||||
|
||||
<link rel="shortcut icon" href="https://picocss.com/favicon.ico">
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.4/css/all.css"
|
||||
integrity="sha384-DyZ88mC6Up2uqS4h/KRgHuoeGwBcD4Ng9SiP4dIRy0EXTlnuz47vAwmeGwVChigm" crossorigin="anonymous"/>
|
||||
|
||||
<!-- Pico.css -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.min.css">
|
||||
<!-- Custom template-->
|
||||
<link rel="stylesheet" href="/stylesheet.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header class="container">
|
||||
<h5>Edit {{selectedFileName}}</h5>
|
||||
<nav>
|
||||
<ul class="breadcrumbs">
|
||||
<li><a href="/"><i class="fa fa-home"></i></a></li>
|
||||
{{#breadcrumbs}}
|
||||
<li><a href="/?path={{fullName}}">{{name}}</a></li>
|
||||
{{/breadcrumbs}}
|
||||
<li>{{selectedFileName}}</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href="javascript:save()" role="button">Save</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Main -->
|
||||
<main class="container">
|
||||
<section id="editor">
|
||||
<figure>
|
||||
<div id="editor"></div>
|
||||
</figure>
|
||||
</section>
|
||||
|
||||
</main><!-- ./ Main -->
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="container">
|
||||
<small>Built with <a href="https://picocss.com">Pico</a> • <a
|
||||
href="https://github.com/picocss/examples/blob/master/basic-template/">Source code</a></small>
|
||||
</footer><!-- ./ Footer -->
|
||||
|
||||
|
||||
<!-- Minimal theme switcher -->
|
||||
<script src="../js/minimal-theme-switcher.js"></script>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.12.5/ace.js"
|
||||
integrity="sha512-gLQA+KKlMRzGRNhdvGX+3F5UHojWkIIKvG2lNQk0ZM5QUbdG17/hDobp06zXMthrJrd4U1+boOEACntTGlPjJQ=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.12.5/ext-modelist.min.js"
|
||||
integrity="sha512-gluOZJrbb4P8hFk1M2HREivZlXwZd0Uf1i5LLt6NRHjjnu7+4eE/yaYH/m82kMTU557+Q2xJhMmqtzMBBh8o/g=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/3.6.2/fetch.min.js"
|
||||
integrity="sha512-1Gn7//DzfuF67BGkg97Oc6jPN6hqxuZXnaTpC9P5uw8C6W4yUNj5hoS/APga4g1nO2X6USBb/rXtGzADdaVDeA=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">
|
||||
|
||||
<script>
|
||||
var modelist = require("ace/ext/modelist");
|
||||
var mode = modelist.getModeForPath("{{selectedFileName}}");
|
||||
var editor = ace.edit("editor",
|
||||
{
|
||||
mode: mode.mode,
|
||||
selectionStyle: "text",
|
||||
theme: "ace/theme/dracula"
|
||||
});
|
||||
document.getElementById('editor').style.fontSize = '1.2em';
|
||||
|
||||
fetch('/content?path={{selectedFile}}')
|
||||
.then(function (response) {
|
||||
return response.text()
|
||||
})
|
||||
.then(function (body) {
|
||||
editor.setValue(body);
|
||||
editor.clearSelection();
|
||||
editor.gotoLine(1);
|
||||
});
|
||||
|
||||
function save() {
|
||||
var blob = new Blob([editor.getValue()]);
|
||||
var formData = new FormData();
|
||||
formData.set("file", blob);
|
||||
fetch('/content?path={{selectedFile}}', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
"{{_csrf.headerName}}": "{{_csrf.token}}"
|
||||
},
|
||||
body: formData,
|
||||
})
|
||||
.then(function(ok) {
|
||||
Toastify({
|
||||
text: "Saved",
|
||||
duration: 3000,
|
||||
close: true,
|
||||
gravity: "top",
|
||||
position: "right",
|
||||
stopOnFocus: true,
|
||||
style: {
|
||||
background: "green",
|
||||
},
|
||||
onClick: function(){} // Callback after click
|
||||
}).showToast();
|
||||
}).catch(function(ex) {
|
||||
Toastify({
|
||||
text: "Error",
|
||||
duration: 3000,
|
||||
close: true,
|
||||
gravity: "top",
|
||||
position: "right",
|
||||
stopOnFocus: true,
|
||||
style: {
|
||||
background: "red",
|
||||
},
|
||||
onClick: function(){} // Callback after click
|
||||
}).showToast();
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
|
||||
</body>
|
||||
</html>
|
||||
126
src/main/resources/templates/login.mustache
Normal file
126
src/main/resources/templates/login.mustache
Normal file
@@ -0,0 +1,126 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Browser">
|
||||
|
||||
<title>Login</title>
|
||||
|
||||
<link rel="shortcut icon" href="https://picocss.com/favicon.ico">
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.4/css/all.css"
|
||||
integrity="sha384-DyZ88mC6Up2uqS4h/KRgHuoeGwBcD4Ng9SiP4dIRy0EXTlnuz47vAwmeGwVChigm" crossorigin="anonymous"/>
|
||||
|
||||
<!-- Pico.css -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.min.css">
|
||||
<!-- Custom template-->
|
||||
<!-- <link rel="stylesheet" href="/stylesheet.css">-->
|
||||
<style>
|
||||
|
||||
/* Grid */
|
||||
body > main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-height: calc(100vh - 7rem);
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
article {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
article div {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
body > main {
|
||||
padding: 1.25rem 0;
|
||||
}
|
||||
|
||||
article div {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
body > main {
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
article div {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
body > main {
|
||||
padding: 1.75rem 0;
|
||||
}
|
||||
|
||||
article div {
|
||||
padding: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
body > main {
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
article div {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Nav */
|
||||
summary[role="link"].secondary:is([aria-current],:hover,:active,:focus) {
|
||||
background-color: transparent;
|
||||
color: var(--secondary-hover);
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.grid > div:nth-of-type(2) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
body > footer {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- Main -->
|
||||
<main class="container">
|
||||
<article class="grid">
|
||||
<div>
|
||||
<hgroup>
|
||||
<h1>Sign in</h1>
|
||||
</hgroup>
|
||||
<form method="post">
|
||||
<input type="text" name="username" placeholder="Username" aria-label="Login" autocomplete="nickname" required>
|
||||
<input type="password" name="password" placeholder="Password" aria-label="Password" autocomplete="current-password" required>
|
||||
<input type="hidden" name="{{_csrf.parameterName}}" value="{{_csrf.token}}"/>
|
||||
<button type="submit" class="contrast">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
<div></div>
|
||||
</article>
|
||||
</main><!-- ./ Main -->
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="container">
|
||||
|
||||
<small>Built with <a href="https://picocss.com">Pico</a> • <a
|
||||
href="https://github.com/picocss/examples/blob/master/basic-template/">Source code</a></small>
|
||||
</footer><!-- ./ Footer -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
13
src/test/java/com/github/russp/bpe/BpeApplicationTests.java
Normal file
13
src/test/java/com/github/russp/bpe/BpeApplicationTests.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.github.russp.bpe;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class BpeApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user