From bab0984c3073a85b92e5cb8037eb8e06c15f9559 Mon Sep 17 00:00:00 2001 From: kunkliricsi Date: Wed, 25 Nov 2020 14:10:47 +0100 Subject: [PATCH] Added logdownloader --- Birdmap.API/ClientApp/src/App.tsx | 19 +- .../src/components/logs/LogService.ts | 201 ++++++++++++++++++ .../ClientApp/src/components/logs/Logs.jsx | 87 ++++++++ Birdmap.API/Controllers/LogsController.cs | 66 ++++++ 4 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 Birdmap.API/ClientApp/src/components/logs/LogService.ts create mode 100644 Birdmap.API/ClientApp/src/components/logs/Logs.jsx create mode 100644 Birdmap.API/Controllers/LogsController.cs diff --git a/Birdmap.API/ClientApp/src/App.tsx b/Birdmap.API/ClientApp/src/App.tsx index dabd34f..4e7d569 100644 --- a/Birdmap.API/ClientApp/src/App.tsx +++ b/Birdmap.API/ClientApp/src/App.tsx @@ -17,6 +17,7 @@ import Devices from './components/devices/Devices'; import { blueGrey, blue, orange, grey } from '@material-ui/core/colors'; import DevicesContextProvider from './contexts/DevicesContextProvider' import Dashboard from './components/dashboard/Dashboard'; +import Logs from './components/logs/Logs'; const theme = createMuiTheme({ @@ -48,6 +49,10 @@ function App() { ); } + const LogsComponent = () => { + return + } + const DashboardComponent = () => { return ; }; @@ -68,7 +73,8 @@ function App() { - + + @@ -90,6 +96,16 @@ const PublicRoute = ({ component: Component, ...rest }: { [x: string]: any, comp ); } +const AdminRoute = ({ component: Component, authenticated: Authenticated, isAdmin: IsAdmin, ...rest }: { [x: string]: any, component: any, authenticated: any, isAdmin: any }) => { + return ( + ( + Authenticated && IsAdmin + ? + : + )} /> + ); +}; + const PrivateRoute = ({ component: Component, authenticated: Authenticated, ...rest }: { [x: string]: any, component: any, authenticated: any }) => { return ( ( @@ -147,6 +163,7 @@ const DefaultLayout = ({ component: Component, authenticated: Authenticated, ... return Authenticated ? Dashboard + {} Devices Heatmap +// Generated using the NSwag toolchain v13.8.2.0 (NJsonSchema v10.2.1.0 (Newtonsoft.Json v12.0.0.0)) (http://NSwag.org) +// +//---------------------- +// ReSharper disable InconsistentNaming + +export default class LogService { + private http: { fetch(url: RequestInfo, init?: RequestInit): Promise }; + private baseUrl: string; + protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined; + + constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise }) { + this.http = http ? http : window; + this.baseUrl = baseUrl !== undefined && baseUrl !== null ? baseUrl : "https://localhost:44331"; + } + + getAll(): Promise { + let url_ = this.baseUrl + "/api/Logs/all"; + url_ = url_.replace(/[?&]$/, ""); + + let options_ = { + method: "GET", + headers: { + "Accept": "application/json", + 'Authorization': sessionStorage.getItem('user') + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processGetAll(_response); + }); + } + + protected processGetAll(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + if (Array.isArray(resultData200)) { + result200 = [] as any; + for (let item of resultData200) + result200!.push(item); + } + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null); + } + + getFiles(filenames: string[] | null | undefined): Promise { + let url_ = this.baseUrl + "/api/Logs?"; + if (filenames !== undefined && filenames !== null) + filenames && filenames.forEach(item => { url_ += "filenames=" + encodeURIComponent("" + item) + "&"; }); + url_ = url_.replace(/[?&]$/, ""); + + let options_ = { + method: "GET", + headers: { + "Accept": "application/octet-stream", + 'Authorization': sessionStorage.getItem('user') + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processGetFiles(_response); + }); + } + + protected processGetFiles(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined; + const fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + const fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null); + } +} + +export interface FileResponse { + data: Blob; + status: number; + fileName?: string; + headers?: { [name: string]: any }; +} + +export enum HttpStatusCode { + Continue = "Continue", + SwitchingProtocols = "SwitchingProtocols", + Processing = "Processing", + EarlyHints = "EarlyHints", + OK = "OK", + Created = "Created", + Accepted = "Accepted", + NonAuthoritativeInformation = "NonAuthoritativeInformation", + NoContent = "NoContent", + ResetContent = "ResetContent", + PartialContent = "PartialContent", + MultiStatus = "MultiStatus", + AlreadyReported = "AlreadyReported", + IMUsed = "IMUsed", + MultipleChoices = "Ambiguous", + Ambiguous = "Ambiguous", + MovedPermanently = "Moved", + Moved = "Moved", + Found = "Redirect", + Redirect = "Redirect", + SeeOther = "RedirectMethod", + RedirectMethod = "RedirectMethod", + NotModified = "NotModified", + UseProxy = "UseProxy", + Unused = "Unused", + TemporaryRedirect = "TemporaryRedirect", + RedirectKeepVerb = "TemporaryRedirect", + PermanentRedirect = "PermanentRedirect", + BadRequest = "BadRequest", + Unauthorized = "Unauthorized", + PaymentRequired = "PaymentRequired", + Forbidden = "Forbidden", + NotFound = "NotFound", + MethodNotAllowed = "MethodNotAllowed", + NotAcceptable = "NotAcceptable", + ProxyAuthenticationRequired = "ProxyAuthenticationRequired", + RequestTimeout = "RequestTimeout", + Conflict = "Conflict", + Gone = "Gone", + LengthRequired = "LengthRequired", + PreconditionFailed = "PreconditionFailed", + RequestEntityTooLarge = "RequestEntityTooLarge", + RequestUriTooLong = "RequestUriTooLong", + UnsupportedMediaType = "UnsupportedMediaType", + RequestedRangeNotSatisfiable = "RequestedRangeNotSatisfiable", + ExpectationFailed = "ExpectationFailed", + MisdirectedRequest = "MisdirectedRequest", + UnprocessableEntity = "UnprocessableEntity", + Locked = "Locked", + FailedDependency = "FailedDependency", + UpgradeRequired = "UpgradeRequired", + PreconditionRequired = "PreconditionRequired", + TooManyRequests = "TooManyRequests", + RequestHeaderFieldsTooLarge = "RequestHeaderFieldsTooLarge", + UnavailableForLegalReasons = "UnavailableForLegalReasons", + InternalServerError = "InternalServerError", + NotImplemented = "NotImplemented", + BadGateway = "BadGateway", + ServiceUnavailable = "ServiceUnavailable", + GatewayTimeout = "GatewayTimeout", + HttpVersionNotSupported = "HttpVersionNotSupported", + VariantAlsoNegotiates = "VariantAlsoNegotiates", + InsufficientStorage = "InsufficientStorage", + LoopDetected = "LoopDetected", + NotExtended = "NotExtended", + NetworkAuthenticationRequired = "NetworkAuthenticationRequired", +} + +export class ApiException extends Error { + message: string; + status: number; + response: string; + headers: { [key: string]: any; }; + result: any; + + constructor(message: string, status: number, response: string, headers: { [key: string]: any; }, result: any) { + super(); + + this.message = message; + this.status = status; + this.response = response; + this.headers = headers; + this.result = result; + } + + protected isApiException = true; + + static isApiException(obj: any): obj is ApiException { + return obj.isApiException === true; + } +} + +function throwException(message: string, status: number, response: string, headers: { [key: string]: any; }, result?: any): any { + if (result !== null && result !== undefined) + throw result; + else + throw new ApiException(message, status, response, headers, null); +} \ No newline at end of file diff --git a/Birdmap.API/ClientApp/src/components/logs/Logs.jsx b/Birdmap.API/ClientApp/src/components/logs/Logs.jsx new file mode 100644 index 0000000..419d969 --- /dev/null +++ b/Birdmap.API/ClientApp/src/components/logs/Logs.jsx @@ -0,0 +1,87 @@ +import { Button, Checkbox, List, ListItem, ListItemIcon, ListItemText, Paper } from '@material-ui/core'; +import React, { Component } from 'react'; +import LogService from './LogService'; + +export class Logs extends Component { + constructor(props) { + super(props) + + this.state = { + service: null, + files: [], + checked: [], + } + } + + componentDidMount() { + var service = new LogService(); + this.setState({service: service}); + + service.getAll().then(result => { + this.setState({files: result}); + }).catch(ex => console.log(ex)); + } + + handleToggle = (value) => { + const currentIndex = this.state.checked.indexOf(value); + const newChecked = [...this.state.checked]; + + if (currentIndex === -1) { + newChecked.push(value); + } else { + newChecked.splice(currentIndex, 1); + } + + this.setState({checked: newChecked}); + } + + downloadChecked = () => { + this.state.service.getFiles(this.state.checked) + .then(result => { + const filename = `Logs-${new Date().toISOString()}.zip`; + const textUrl = URL.createObjectURL(result.data); + const element = document.createElement('a'); + element.setAttribute('href', textUrl); + element.setAttribute('download', filename); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + }) + .catch(ex => console.log(ex)); + } + + render() { + const Files = this.state.files.map((value) => { + const labelId = `checkbox-list-label-${value}`; + + return ( + this.handleToggle(value)}> + + + + + + ); + }) + + return ( + + + {Files} + + + + ) + } +} + +export default Logs diff --git a/Birdmap.API/Controllers/LogsController.cs b/Birdmap.API/Controllers/LogsController.cs new file mode 100644 index 0000000..0e96c3e --- /dev/null +++ b/Birdmap.API/Controllers/LogsController.cs @@ -0,0 +1,66 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Birdmap.API.Controllers +{ + [Authorize(Roles = "Admin")] + [ApiController] + [Route("api/[controller]")] + public class LogsController : ControllerBase + { + private readonly ILogger _logger; + private readonly string _logFolderPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Log"); + + public LogsController(ILogger logger) + { + _logger = logger; + } + + [HttpGet("all")] + public ActionResult> GetAll() + { + _logger.LogInformation($"Getting all log filenames..."); + + return Directory.EnumerateFiles(_logFolderPath, "*.log") + .Select(f => Path.GetFileName(f)) + .ToList(); + } + + [HttpGet] + public async Task GetFiles([FromQuery] params string[] filenames) + { + if (!filenames.Any()) + return null; + + return await Task.Run(() => + { + var zipStream = new MemoryStream(); + + using (var zip = new ZipArchive(zipStream, ZipArchiveMode.Create, true)) + { + foreach (var file in Directory.GetFiles(_logFolderPath, "*.log")) + { + var filename = Path.GetFileName(file); + + if (filenames.Contains(filename)) + { + zip.CreateEntryFromFile(file, filename); + } + } + } + + zipStream.Position = 0; + + return File(zipStream, "application/octet-stream"); + }); + } + } +}