From 6a16bbdcdb40406592e47ee8d489f857837e5c96 Mon Sep 17 00:00:00 2001 From: Vikas Kushwaha Date: Thu, 21 Nov 2024 13:54:38 +0530 Subject: Initial commit --- .gitignore | 1 + report/cloud-maker.pdf | Bin 0 -> 759361 bytes report/cloud-maker.tex | 824 ++++++++++++++++++++++ report/images/dupli-checker.png | Bin 0 -> 118040 bytes report/images/ss/1.cm-login.png | Bin 0 -> 18644 bytes report/images/ss/2.cm-home.png | Bin 0 -> 61787 bytes report/images/ss/3.cm-upload.png | Bin 0 -> 20912 bytes report/images/ss/4.cm-create-folder.png | Bin 0 -> 24152 bytes report/images/ss/5.cm-select.png | Bin 0 -> 63831 bytes report/images/ss/6.cm-cut.png | Bin 0 -> 71320 bytes report/images/ss/7.cm-cut-copy.png | Bin 0 -> 92486 bytes report/images/ss/8.cm-delete-prompt.png | Bin 0 -> 50493 bytes report/images/vartak-logo.jpg | Bin 0 -> 35836 bytes report/images/vartak-logo.svg | 5 + scripts/automount-clean | 9 + scripts/automountd | 19 + src/copy.go | 124 ++++ src/files.go | 449 ++++++++++++ src/go.mod | 5 + src/go.sum | 2 + src/helpers.go | 292 ++++++++ src/server.go | 298 ++++++++ src/static/icons/bs/actions/arrow-left-short.svg | 3 + src/static/icons/bs/actions/arrow-left.svg | 3 + src/static/icons/bs/actions/arrow-right-short.svg | 3 + src/static/icons/bs/actions/arrow-right.svg | 3 + src/static/icons/bs/actions/backspace.svg | 4 + src/static/icons/bs/actions/check.svg | 3 + src/static/icons/bs/actions/clipboard.svg | 4 + src/static/icons/bs/actions/copy.svg | 3 + src/static/icons/bs/actions/download.svg | 4 + src/static/icons/bs/actions/folder-plus.svg | 4 + src/static/icons/bs/actions/folder.svg | 3 + src/static/icons/bs/actions/scissors.svg | 3 + src/static/icons/bs/actions/trash.svg | 4 + src/static/icons/bs/actions/upload.svg | 4 + src/static/icons/bs/files/file-earmark.svg | 3 + src/static/icons/bs/files/folder-symlink.svg | 4 + src/static/icons/bs/files/folder2.svg | 3 + src/static/icons/bs/files/link-45deg.svg | 4 + src/static/icons/bs/files/link-broken-45deg.svg | 4 + src/static/icons/bs/files/question.svg | 3 + src/static/icons/bs/hdd.svg | 4 + src/static/icons/bs/house-door.svg | 3 + src/static/icons/bs/person-circle.svg | 4 + src/static/script.js | 41 ++ src/static/style.css | 356 ++++++++++ src/templates.go | 45 ++ src/templates/base.html | 17 + src/templates/profile.html | 24 + src/templates/viewDir.html | 106 +++ src/templates/viewHome.html | 29 + 52 files changed, 2726 insertions(+) create mode 100644 .gitignore create mode 100644 report/cloud-maker.pdf create mode 100644 report/cloud-maker.tex create mode 100644 report/images/dupli-checker.png create mode 100644 report/images/ss/1.cm-login.png create mode 100644 report/images/ss/2.cm-home.png create mode 100644 report/images/ss/3.cm-upload.png create mode 100644 report/images/ss/4.cm-create-folder.png create mode 100644 report/images/ss/5.cm-select.png create mode 100644 report/images/ss/6.cm-cut.png create mode 100644 report/images/ss/7.cm-cut-copy.png create mode 100644 report/images/ss/8.cm-delete-prompt.png create mode 100644 report/images/vartak-logo.jpg create mode 100644 report/images/vartak-logo.svg create mode 100755 scripts/automount-clean create mode 100755 scripts/automountd create mode 100644 src/copy.go create mode 100644 src/files.go create mode 100644 src/go.mod create mode 100644 src/go.sum create mode 100644 src/helpers.go create mode 100644 src/server.go create mode 100644 src/static/icons/bs/actions/arrow-left-short.svg create mode 100644 src/static/icons/bs/actions/arrow-left.svg create mode 100644 src/static/icons/bs/actions/arrow-right-short.svg create mode 100644 src/static/icons/bs/actions/arrow-right.svg create mode 100644 src/static/icons/bs/actions/backspace.svg create mode 100644 src/static/icons/bs/actions/check.svg create mode 100644 src/static/icons/bs/actions/clipboard.svg create mode 100644 src/static/icons/bs/actions/copy.svg create mode 100644 src/static/icons/bs/actions/download.svg create mode 100644 src/static/icons/bs/actions/folder-plus.svg create mode 100644 src/static/icons/bs/actions/folder.svg create mode 100644 src/static/icons/bs/actions/scissors.svg create mode 100644 src/static/icons/bs/actions/trash.svg create mode 100644 src/static/icons/bs/actions/upload.svg create mode 100644 src/static/icons/bs/files/file-earmark.svg create mode 100644 src/static/icons/bs/files/folder-symlink.svg create mode 100644 src/static/icons/bs/files/folder2.svg create mode 100644 src/static/icons/bs/files/link-45deg.svg create mode 100644 src/static/icons/bs/files/link-broken-45deg.svg create mode 100644 src/static/icons/bs/files/question.svg create mode 100644 src/static/icons/bs/hdd.svg create mode 100644 src/static/icons/bs/house-door.svg create mode 100644 src/static/icons/bs/person-circle.svg create mode 100644 src/static/script.js create mode 100644 src/static/style.css create mode 100644 src/templates.go create mode 100644 src/templates/base.html create mode 100644 src/templates/profile.html create mode 100644 src/templates/viewDir.html create mode 100644 src/templates/viewHome.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build diff --git a/report/cloud-maker.pdf b/report/cloud-maker.pdf new file mode 100644 index 0000000..e3fe6e4 Binary files /dev/null and b/report/cloud-maker.pdf differ diff --git a/report/cloud-maker.tex b/report/cloud-maker.tex new file mode 100644 index 0000000..d6179c2 --- /dev/null +++ b/report/cloud-maker.tex @@ -0,0 +1,824 @@ +\documentclass[12pt]{article} +\usepackage[top=2cm, left=3cm, right=3cm]{geometry} +\usepackage[utf8]{inputenc} +\usepackage[english]{babel} +\usepackage{times} +\usepackage{calc} +\usepackage{eso-pic} +\usepackage{titlesec} +\usepackage{soulutf8} +\usepackage{array} +\usepackage{makecell} +\usepackage{hyperref} +\usepackage{graphicx} +\usepackage{tikz-uml} +\usepackage{pgfgantt} +\usepackage{listings} +\usepackage{color} + +\date{06 Oct, 2024} +\title{Cloud Maker} +\author{Vikas Kushwaha} + +\nonstopmode + +\newlength{\PageFrameTopMargin} +\newlength{\PageFrameBottomMargin} +\newlength{\PageFrameLeftMargin} +\newlength{\PageFrameRightMargin} + +\setlength{\PageFrameTopMargin}{1cm} +\setlength{\PageFrameBottomMargin}{1cm} +\setlength{\PageFrameLeftMargin}{1cm} +\setlength{\PageFrameRightMargin}{1cm} + +\makeatletter + +\let\inserttitle\@title +\let\insertauthor\@author +\let\insertdate\@date + +\newlength{\Page@FrameHeight} +\newlength{\Page@FrameWidth} + +\AddToShipoutPicture { + \thicklines + \setlength{\Page@FrameHeight}{\paperheight-\PageFrameTopMargin-\PageFrameBottomMargin} + \setlength{\Page@FrameWidth}{\paperwidth-\PageFrameLeftMargin-\PageFrameRightMargin} + \put(\strip@pt\PageFrameLeftMargin,\strip@pt\PageFrameTopMargin){ + \framebox(\strip@pt\Page@FrameWidth, \strip@pt\Page@FrameHeight){}}} + +\makeatother + +\titleformat{\section} +{\Large\bfseries\center\uppercase} +{} +{1em} +{} +[\hrule] + +\titleformat{\subsubsection} +{\bfseries\uppercase} +{} +{1em} +{\ul} + +\AddToHook{cmd/section/before}{\clearpage} +\graphicspath{ {./images/} } + +\newenvironment{changemargin}[2]{% +\begin{list}{}{% +\setlength{\topsep}{0pt}% +\setlength{\leftmargin}{#1}% +\setlength{\rightmargin}{#2}% +\setlength{\listparindent}{\parindent}% +\setlength{\itemindent}{\parindent}% +\setlength{\parsep}{\parskip}% +}% +\item[]}{\end{list}} + +\renewcommand\theadfont{\bfseries} +\newcolumntype{L}[1]{>{\raggedright\let\newline\\\arraybackslash\hspace{0pt}}m{#1}} +\newcolumntype{C}[1]{>{\centering\let\newline\\\arraybackslash\hspace{0pt}}m{#1}} +\newcolumntype{R}[1]{>{\raggedleft\let\newline\\\arraybackslash\hspace{0pt}}m{#1}} + + +\definecolor{dkgreen}{rgb}{0,0.6,0} +\definecolor{gray}{rgb}{0.5,0.5,0.5} +\definecolor{mauve}{rgb}{0.58,0,0.82} + +\lstset{frame=tb, + language=Go, + aboveskip=3mm, + belowskip=3mm, + showstringspaces=false, + columns=flexible, + basicstyle={\small\ttfamily}, + numbers=none, + numberstyle=\tiny\color{gray}, + keywordstyle=\bfseries\color{blue}, + commentstyle=\itshape\color{dkgreen}, + identifierstyle=\color{mauve}, + stringstyle=\color{red}, + breaklines=true, + breakatwhitespace=true, + tabsize=3 +} + +\hypersetup{colorlinks=true, urlcolor=blue} + + +\begin{document} + + +\iftrue % Theory + + +\begin{center} + \fontsize{14pt}{28pt}\selectfont + A PROJECT REPORT \\ + on \\ + \textbf{"\underline{\MakeUppercase{\inserttitle}}"} \\ + \textit{submitted by} \\ + \textbf{Mr. \insertauthor} \\ + \textbf{Seat No :-} \\ + \textit{in partial fullfillment for the award of the degree} \\ + of \\ + \textbf{BACHELOR OF SCIENCE} \\ + in \\ + \textbf{COMPUTER SCIENCE} \\ + \textit{under the guidance of} \\ + \textbf{Mrs. Swetha Iyer} \\ + \textbf{Department of Computer Science} \\ + \vspace{1cm} + \includegraphics[scale=0.25]{vartak-logo} \\ + \fontsize{14pt}{20pt}\selectfont + \textbf{VIDYAVARDHINI'S} \\ + \textbf{A. V. COLLEGE OF ARTS, K. M. COLLEGE OF COMMERCE} \\ + \textbf{E. S. A. COLLEGE OF SCIENCE,} \\ + \textbf{VASAI(WEST), PALGHAR-401208, MAHARASHTRA} \\ + \fontsize{14pt}{28pt}\selectfont + \textbf{(Sem V)} \\ + \textbf{(2024-25)} \\ +\end{center} + + +\fontsize{12pt}{24pt}\selectfont + +\section{Acknowledgement} +\begin{center} + \vspace{2cm} + I would like to acknowledge my sincere thanks towards our project guide \\ + \textbf{Mrs. Swetha Iyer} \\ + for their valuable guidance and suggestions and \\ + providing me an opportunity to do the project work in the college lab and \\ + which made me complete the project successfully. \\ + \vspace{2cm} + I am also thankful to \\ + \textbf{Head of Computer Scince Department} \\ + \textbf{Mrs. Srimathi Narayanan} \\ + For providing such nice guidance in form of comments and corrections. \\ + I am thakful to and fortunate enough to get contant encouragement, \\ + support and guidance from all teaching staff of Computer Science \\ + which helped us in successfully completing our project work. \\ +Also, I would like to extend our sincere esteems to all staff in laboratory \\ + for their timely support. \\ + \vspace{2cm} + By \textbf{\insertauthor}, \\ + T.Y.BSc (Computer Science) +\end{center} + + +\section{Declaration} +\vspace{2cm} +I \textbf{\underline{\insertauthor}} hereby declare that, \\ +\bigskip \\ +The project entitled +\textbf{"\underline{\MakeUppercase{\inserttitle}}"} +submitted in the partial fulfillment for the award of Bachelor of Science in Computer Science during the academic year \textbf{2024 - 2025} is my original work and the project has not formed the basis for the award of any degree, associate ship, fellowhip or any other similar titles. \\ \\ \\ +\vspace{2cm} +\textbf{Signature of the Student:} \\ +\bigskip +\textbf{Place:} \\ +\bigskip +\textbf{Date:} \\ + + +\section{Plagarism Report} +\vspace{2cm} +\includegraphics[width=\linewidth]{dupli-checker} \\ +... + + +\section{Gantt Chart} + + +\section{Table Of Content} +\vfill +\begin{center} +\begin{tabular}{ | C{2cm} | m{8cm} | C{2cm} | m{2cm} | } + \hline + \thead{Sr. No} & \thead{Contents} & \thead{Page No.} & \thead{Sign} \\ + \hline + \hline\textbf{1.} & \textbf{Introduction} & \textbf{7} & \\ + \hline\textbf{2.} & \textbf{Limitation of Current System} & \textbf{8} & \\ + \hline\textbf{3.} & \textbf{Advantages of Proposed System} & \textbf{9} & \\ + \hline\textbf{4.} & \textbf{Tools and Techniques} & \textbf{10} & \\ + \hline\textbf{5.} & \textbf{Requirement Specification} & \textbf{11} & \\ + \hline\textbf{6.} & + \bgroup + \def\arraystretch{0.7}% + \begin{tabular}{L{0.7cm} l} + & \textbf{System Design} \\ + \textbf{(A)} & Event Table \\ + \textbf{(B)} & ER Diagram \\ + \textbf{(C)} & Class Diagram \\ + \textbf{(D)} & Use Case Diagram \\ + \textbf{(E)} & Sequence Diagram \\ + \textbf{(F)} & Component Diagram \\ + \textbf{(G)} & Deployment Diagram \\ + \textbf{(H)} & Activity Diagram \\ + \textbf{(I)} & Database \\ + \end{tabular} + \egroup + & \textbf{12 - 20} & \\ + \hline\textbf{7.} & \textbf{System Implementation} & \textbf{21 - 47} & \\ + \hline\textbf{8.} & \textbf{Results} & \textbf{48 - 51} & \\ + \hline\textbf{9.} & \textbf{Conclusion} & \textbf{52} & \\ + \hline\textbf{10.} & \textbf{References} & \textbf{53} & \\ + \hline +\end{tabular} +\end{center} +\vfill + + +\section{Introduction} +\vspace{1cm} +\textbf{\uppercase{Title of the Project:}} \quad\textbf{\MakeUppercase{\inserttitle}} +\bigskip +\subsubsection{Synopsis:} \quad\quad +\textit{A cloud storage in distributed fashion.} \\ \par +This system is intended to be an alternative to online storage proveders like Google Drive. It's a distributed model where the user physically owns the resources in his/her own house. Unlike centralised cloud providers, the user is given a small computing device, preferrably an SBC like Rasberry Pi which acts as an 'Endpoint Device' and a gateway to access user's various media devices like Pen Drives, Hard Drives, Memory Cards, and any other sotrage media that can potentially interface with the Endpoint Device (Rasberry Pi, in our case.) \\ \par +The user is intended to connect his Endpoint device using a Web Proxy which will be automatically setup acting as a Internet Gateway to his Endpoint Device. The Rasberry Pi will be primary product for the user that will act as a Cloud Storage Provider. He can access this Cloud Storage from any computer that has an Internet Connection. The user will be provided with a Web Interface from where he can view and manage all his Files and Folders. \\ +\par +In summary, it turns the user's own storage devices into a cloud making it convinient for the user to access his storage from anywhere in the world. + + +\section{Limitations of Current System} +\vspace{2cm} +\quad\quad Thc cloud storage space have now become mainstream. People used store their files and data backups on External Storage Devices like Pend Drives and Hard Drives. However, these days we just upload all our content to cloud storage like Google Drive. This poses many problems and risks surrounding around Data Privacy, Security and Ownership of Data. It's now a well known fact that companies sell the User Data they harvest to other companies in exchange of profits. \\ + +There's also a problem with the costs involved. Cloud Storage Drives are extremely Expensive. Just for instance Google Cloud charges you Rs. 130 per month in India for a 100GB storage space for a single user. Assuming a year is just 10 months, it becomes Rs. 1300 for an year an Rs. 13,000 for a decade. That's actually quiet expensive for just a 100GB of storage space. A lot of people aren't really interested in spending a lot of money on such storage options as they often simply can't afford it. They will usually try to limit themselves by just using the limited storage space that is provided per account for free. 15 GigaBytes in case of Google Drive. + + +\section{Advantages of Proposed System} +\vspace{2cm} +\quad\quad This system tries to solve many of the problems with Cloud Storage proveders, as mentioned in the previous section that revolve araound data privacy and security and also costs. \\ +\par +\inserttitle{} ensures that the data stays on user's physical medium ensuring that the user absolutely owns the data and no one else on the internet has access to it. Unless ofcourse, they had physical access to the device itself. This is incredibly useful for storing highly sensitive documents and other details that can be detremental for the user if fallen onto wrong hands. Being distributed in nature, it also minimizes the damage of data breaches. If a hacker do succeed in any case breaching the cloud storage, they only breache one device rather than breaching the whole community. This can be quite important for Government officials storing classified documents on the storage devices \\ +\par +\inserttitle{} also makes it feasible for users to have large cloud storage space. As the user can simply use his/her own storage devices as a cloud store. The user can buy 1 TeraByte hard drive which will usually costs around Rs. 3,000 to 5,000 and can easily last for 6+ years and even a decade. This makes the costs and scale feasible for the user. This is especially useful for professionals like Video Editors and Graphics designers that often have Adboe project files spanning over multiple GigaBytes. They can't carry their hard drive everywhere and they often have to access various of their previous workd sporadically even for new projects. + + +\section{Tools and Techniques} +\vspace{2cm} +\quad\quad +This system involves an Endpoint Server and a client computer. For the endpoint server, let's assume a Rasberry Pi Model 3B flashed with a Linux Operating System like Debian 10 for ARM. This devices primarily runs two things, a web server and a set of scripts for managing storage devices. It then forwards it's Web Server Port to a VPS server using SSH. This VPS server has a pre-registered domain name and acts as a proxy to the Rasberry Pi Endpoint making it accessible throught the internet. \\ +\par +Initially, two shell scripts are started in the bacground -- automountd and automount-clean. automountd monitors all the usb ports of Rasberry Pi and automatically mounts any storage device as soon as it's connected to it. It mounts them at a specific mount point (/media/user) which is later used by the Web Server. automount-clean monitors the mount point and cleans any dangling directories left over of unmount storage devices. \\ +\par +The Web Server makes the mount point accessible to other computers by providing a web interface for browsing and managing files. The Web Interface is essentially a File Manager. The Web Server is written in Go Programming Languages and uses http router from standard library which provides routing functionality and html/template (Go's built in template engine) which provides the building blocks of the web interface.\\ +\par +The Web Interface can be accessed through any computer or mobile device. The user can freely upload, download, or share files from his media device. + + +\section{Requirement Specification} +\vspace{2cm} +\begin{enumerate} + \item \textbf{Hardware Requirements:} \\ \\ + For Endpoint Server, + \begin{itemize} + \item Rasberry PI Model B+ or newer + \item 16GB Memory Card + \item External Storage Drives of User Preferred Size + \end{itemize} + For Client Device, + \begin{itemize} + \item Connection to Endpoint Rasberry Pi Server (LAN / WAN) + \end{itemize} + + \vspace{1cm} + \item \textbf{Software Requirements:} \\ \\ + For Endpoint Server, + \begin{itemize} + \item OS: Debian Raspi Linux + \item Shell: Bash + \item Programming Language: Go + \end{itemize} + For Client Device, + \begin{itemize} + \item Any OS with latest Web Browser + \end{itemize} +\end{enumerate} + +\fi % Theory + + +\iftrue % UML Diagrams +\fontsize{12pt}{16pt}\selectfont + +\section{Event Table} +\begin{changemargin}{-1cm}{-1cm} +\vfill +\begin{center} +\begin{tabular}{ | C{2cm} | C{3cm} | C{2cm} | C{3cm} | C{3cm} | C{2cm} | } + \hline + \thead{Event} & + \thead{Trigger} & + \thead{Source} & + \thead{Activity} & + \thead{Response} & + \thead{Destination} \\ + \hline\hline + Cut Files & + User clicks on Cut Button & User & + Files are added to Cut Buffer & + Files in Cut Buffer & + Endpoint Server \\ \hline + + Copy Files & User clicks on Copy Button & User & + Files are added to Copy Buffer & + Files in Copy Buffer & + Endpoint Server \\ \hline + + Paste from Cut Buffer & + User clicks on Paste button & + User & + Files are moved from Cut Buffer & + Files Moved & + Endpoint Server \\ \hline + + Paste from Copy Buffer & + User clicks on Paste button & + User & + Files are copied from Copy Buffer & + Files Copied & + Endpoint Server \\ \hline + + Delete Files & + User clicks on Delete Button & + User & + Selected Files are set to deletion & + Confirm Deletion & + Endpoint Server \\ \hline + + Upload Local Files & + User clicks on Upload Button & + User & + File Browser is opened for selection & + User submits files & + User \\ \hline + + Download Remote Files & + Server gets Download Requests & + Endpoint Server & + Server ZIPs requested file and sends to user & + Compressed File recieved & + User \\ \hline + + Create Folder & + User enters New Folder Name & + User & + New Folder is created on the system & + Folder is shown & + Endpoint Server \\ \hline + +\end{tabular} +\end{center} +\vfill +\end{changemargin} + + +\section{ER Diagram} +\vfill +\begin{center} +\begin{tikzpicture}[ + class/.style={minimum width=4cm}, + ] + \umlclass[class, rectangle split parts=2]{FileSystem}{ + CutCount \\ + CopyCount \\ + FileCount \\ + CutBuffer \\ + CopyBuffer \\ + File \\ + }{} + \umlclass[class, x=8cm, y=0cm, + rectangle split parts=2]{Directory}{ + URI \\ + Path \\ + Info \\ + Files \\ + }{} + \umlclass[class, x=8cm, y=-8cm, + rectangle split parts=2]{File}{ + URI \\ + Path \\ + Info \\ + Data \\ + }{} + \umluniassoc[mult=1]{FileSystem}{Directory} + \umluniassoc[mult=0..*]{Directory}{Directory} + \umluniassoc[mult=0..*]{Directory}{File} +\end{tikzpicture} +\end{center} +\vfill + + +\section{Class Diagram} +\vfill +\begin{center} +\begin{tikzpicture}[ + class/.style={minimum width=6cm}, + ] + \umlclass[class]{FSData}{ + CutCount: int \\ + CopyCount: int \\ + FileCount: int \\ + CutBuffer: []string \\ + CopyBuffer: []string \\ + File: *FileNode \\ + }{} + \umlclass[class, x=8cm, y=-8cm]{FileNode}{ + URI: string \\ + Path: string \\ + IsDir: bool \\ + Info: os.FileInfo \\ + Data: any \\ + }{ + HTMLPath(): template.HTML \\ + EvalSymlinks(): string \\ + IconPath(): string \\ + Size(): string \\ + Mode(): string \\ + ModDate(): string \\ + ModTime(): string \\ + Details(): string \\ + } + \umluniaggreg[geometry=-|]{FSData}{FileNode} +\end{tikzpicture} +\end{center} +\vfill + + +\section{Use Case Diagram} +\vfill +\begin{center} +\begin{tikzpicture}[ + case/.style={text width=3cm}, + subcase/.style={minimum width=3cm} + ] + \begin{umlsystem}[x=6] {} + \umlusecase[case, name=br] {Browse Files Remotely} + \umlusecase[case, name=ob, x=6cm, y=-2cm] {Open in Browser} + \umlusecase[case, name=up, y=-3cm] {Upload Files} + \umlusecase[case, name=dn, y=-5cm] {Download Files} + \umlusecase[case, name=fm, y=-8cm] {Manage Files} + \umlusecase[subcase, name=cp, x=6cm, y=-6cm] {Copy} + \umlusecase[subcase, name=mv, x=7cm, y=-8cm] {Move} + \umlusecase[subcase, name=dl, x=6cm, y=-10cm] {Delete} + \end{umlsystem} + \node [above] at (current bounding box.north) {Cloud Maker}; + \umlactor[y=-4] {User} + \umlassoc{User}{br} + \umlassoc{User}{up} + \umlassoc{User}{dn} + \umlassoc{User}{fm} + \umlextend{br}{ob} + \umlinclude{fm}{cp} + \umlinclude{fm}{mv} + \umlinclude{fm}{dl} +\end{tikzpicture} +\end{center} +\vfill + + +\section{Sequence Diagram} +\vfill +% Stage 1: Connect to Cloud Storage \\ +\begin{center} +\begin{tikzpicture} +\begin{umlseqdiag} + \umlactor[class=User]{u} + \umlobject[x=7, class=Server]{proxy} + \umlobject[x=14, class=Server]{endpoint} + + \begin{umlcall}[op=Port Forwarding, type=synchron, return=Connection Established]{endpoint}{proxy} \end{umlcall} + \begin{umlcall}[op=HTTP Connection Request, type=synchron, dt=10, return=Connection Established]{u}{proxy} + \begin{umlcall}[op=Forward Request, type=synchron, return=HTTP Connection]{proxy}{endpoint} \end{umlcall} + \end{umlcall} +\end{umlseqdiag} +\end{tikzpicture} +\end{center} +\vfill + +\vfill +% Stage 2: Open Files and perform Actions \\ +\begin{center} +\begin{tikzpicture} +\begin{umlseqdiag} + \umlactor[class=User]{u} + \umlobject[x=7, class=Server]{endpoint} + \umlmulti[x=14, class=Drives]{storage} + + \begin{umlcall}[op=Get Filesystem Data]{storage}{endpoint} + \begin{umlcall}[op=File Browser, padding=5, return=Close Connection]{endpoint}{u} + + \begin{umlcall}[op=Upload Files, dt=5]{u}{endpoint} + \begin{umlcall}[op=Save Files, dt=0]{endpoint}{storage} \end{umlcall} + \end{umlcall} + + \begin{umlcallself}[op=Select Files, dt=0]{u} \end{umlcallself} + + \begin{umlcall}[op=Cut/Copy Files, dt=0]{u}{endpoint} \end{umlcall} + \begin{umlcallself}[op=Add Files to Buffer, dt=-2]{endpoint} \end{umlcallself} + + \begin{umlcall}[op=Open Folder, dt=4, padding=0, return=Reload Page]{u}{endpoint} + \begin{umlcall}[op=Change Directory, dt=0]{endpoint}{storage} \end{umlcall} + \end{umlcall} + + \begin{umlcall}[op=Paste Files in buffer]{u}{endpoint} \end{umlcall} + \begin{umlcall}[op=Move/Copy Files in Buffer, dt=-2]{endpoint}{storage} \end{umlcall} + + \begin{umlcall}[op=Download Files, padding=0, return=Files Transferred]{u}{endpoint} + \begin{umlcall}[op=Zip Files, dt=0, return=Files Compressed]{endpoint}{storage} \end{umlcall} + \end{umlcall} + + \begin{umlcall}[op=Delete Files, dt=5, return=Request Confirmation]{u}{endpoint} \end{umlcall} + \begin{umlcall}[op=Confirm]{u}{endpoint} \end{umlcall} + \begin{umlcall}[op=Remove Files, dt=-2]{endpoint}{storage} \end{umlcall} + + \end{umlcall} + \end{umlcall} +\end{umlseqdiag} +\end{tikzpicture} +\end{center} +\vfill + + +\section{Component Diagram} +\vfill +\begin{center} +\begin{tikzpicture}[ + comp/.style={minimum width=3cm} + ] + \umlbasiccomponent[comp, x=0, y=-0]{Client} + \umlbasiccomponent[comp, x=10, y=-0]{Nginx Proxy} + \umlbasiccomponent[comp, x=5, y=-3]{SSH Server} + \umlbasiccomponent[comp, x=0, y=-6]{SSH Client} + \umlbasiccomponent[comp, x=10, y=-6]{Web Server} + \umlbasiccomponent[comp, x=5, y=-9]{Linux OS} + \umlbasiccomponent[comp, x=10, y=-12]{USB Monitor} + % \umlbasiccomponent[comp, x=0, y=-14]{File System} + \umlbasiccomponent[comp, x=0, y=-12]{Storage Devices} + \umlassemblyconnector[interface=HTTP Connection]{Client}{Nginx Proxy} + \umlVHassemblyconnector[interface=Forwarded Port]{Nginx Proxy}{SSH Server} + \umlVHassemblyconnector[interface=SSH Connection]{SSH Client}{SSH Server} + \umlassemblyconnector[interface=Web Server Port]{SSH Client}{Web Server} + \umlHVassemblyconnector[interface=File System]{Linux OS}{Storage Devices} + \umlHVassemblyconnector[interface=USB Ports]{USB Monitor}{Linux OS} + \umlVHassemblyconnector[interface=File System]{Web Server}{Linux OS} +\end{tikzpicture} +\end{center} +\vfill + + +\section{Deployment Diagram} +\vfill +\begin{center} +\begin{tikzpicture} + \begin{umlcomponent}{Client Device} + \umlbasiccomponent[]{Web Browser} + \end{umlcomponent} + \begin{umlcomponent}{Central Proxy Server} + \umlbasiccomponent[x=8]{SSH Server} + \umlbasiccomponent[x=12]{Nginx} + \umlbasiccomponent[x=10, y=-2]{Linux OS} + \end{umlcomponent} + \begin{umlcomponent}{Endpoint Server} + \umlbasiccomponent[x=2, y=-8]{Web Server} + \umlbasiccomponent[x=6, y=-8]{SSH Client} + \umlbasiccomponent[x=10, y=-8]{Linux OS} + \umlbasiccomponent[x=4, y=-10]{USB Monitor} + \umlbasiccomponent[x=8, y=-10]{File System} + \end{umlcomponent} + \begin{umlcomponent}{External Storage} + \umlbasiccomponent[x=0, y=-16]{Pen Drive} + \umlbasiccomponent[x=4, y=-16]{External Hard Drive} + \umlbasiccomponent[x=8, y=-16]{SD Card} + \umlbasiccomponent[x=12, y=-16]{External SSD} + \end{umlcomponent} + \umluniassoc{Client Device}{Central Proxy Server} + \umluniassoc{Endpoint Server}{Central Proxy Server} + \umluniassoc{Endpoint Server}{External Storage} +\end{tikzpicture} +\end{center} +\vfill + + +\section{Activity Diagram} +\vfill +\begin{center} +\begin{tikzpicture}[ + node distance=2cm, + arrow/.style={thick,->,>=stealth}, + start/.style={fill=black,circle,thick}, + label/.style={rectangle, rounded corners=5, text centered, draw=black, fill=yellow!20}, + fixlabel/.style={label, text width=2cm}, + decision/.style={diamond, draw=black, fill=blue!20}, + ] + \node[start] at (0, 0) (start) {}; + \node[fixlabel, below of=start] (browse) {Browse Files}; + \node[fixlabel, left of=browse, xshift=-2cm] (ofolder) {Open\\File}; + \node[fixlabel, right of=browse, xshift=2cm] (ofile) {Upload File}; + \node[decision, below of=browse, yshift=0.5cm] (split1) {}; + \node[label, left of=split1, yshift=-1.5cm, text width=3cm] (select) {Select Files and Folders}; + \node[fixlabel, right of=split1, yshift=-1.5cm] (action) {Perform Action}; + \node[label, right of=action, xshift=2cm] (download) {Download}; + \node[fixlabel, below of=action] (manage) {Manage Files}; + \node[label, left of=manage, xshift=-4cm] (delete) {Delete}; + \node[decision, below of=manage, yshift=0.5cm] (split2) {}; + \node[fixlabel, left of=split2, yshift=-1.5cm] (cut) {Cut to Buffer}; + \node[fixlabel, right of=split2, yshift=-1.5cm] (copy) {Copy to Buffer}; + \node[decision, below of=manage, yshift=-2.5cm] (split3) {}; + \node[fixlabel, below of=split3] (paste) {Paste from Buffer}; + \node[decision, below of=delete, yshift=-0.5cm, fill=red!20] (conf1) {Confirm?}; + \node[label, below of=conf1, yshift=-0.5cm] (conf1yes) {Delete Files}; + \node[label, left of=conf1, xshift=-2cm] (conf1no) {Ignore}; + + \draw[arrow] (start) -- (browse); + \draw[arrow] (browse) -- (ofolder); + \draw[arrow] (browse) -- (ofile); + \draw[arrow] (browse) -- (split1); + \draw[arrow] (split1) -| (select); + \draw[arrow] (split1) -| (action); + \draw[arrow] (select) -- (action); + \draw[arrow] (action) -- (manage); + \draw[arrow] (action) -- (download); + \draw[arrow] (manage) -- (split2); + \draw[arrow] (split2) -| (cut); + \draw[arrow] (split2) -| (copy); + \draw[arrow] (cut) |- (split3); + \draw[arrow] (copy) |- (split3); + \draw[arrow] (split3) -- (paste); + \draw[arrow] (manage) -- (delete); + \draw[arrow] (delete) -- (conf1); + \draw[arrow] (conf1) -- node[anchor=east] {Yes} (conf1yes); + \draw[arrow] (conf1) -- node[anchor=south] {No} (conf1no); +\end{tikzpicture} +\end{center} +\vfill + + +\section{Database} +\vfill +\begin{center} +Representation of File Metadata in File System. \\ +\bigskip +\begin{tabular}{ | m{4cm} | m{3cm} | m{3cm} | m{3cm} | } + \hline + \thead{Attribute} & + \thead{DataType} & + \thead{Size} & + \thead{Retrieval} \\ + \hline\hline + Name & Chars & 255 Bytes & Primary Key \\ + Permissions & Octal & 4 Bytes & Not Null \\ + User UID & Integer & 4 Bytes & Not Null \\ + Group GID & Integer & 4 Bytes & Not Null \\ + Modification Time & Time & 4 Bytes & Not Null \\ \hline +\end{tabular} +\end{center} +\vfill + + +% \section{Gantt Chart} +% \vfill +% \begin{center} +% \begin{ganttchart}[ +% hgrid=true, +% vgrid={*2{dotted}, {dashed}, *3{dotted}, {dashed}, *4{dotted}, {dashed}}, +% y unit chart=0.8cm, +% title height=1, +% expand chart=\textwidth, +% bar label node/.append style={align=right}, +% bar height=0.5, +% exed/.style={bar top shift=0.5}, +% real/.style={bar top shift=0.1, bar/.append style={fill=black}}, +% ]{1}{15} +% \gantttitle{2024}{15} \ganttnewline[draw=none] +% \gantttitle{June}{3} +% \gantttitle{July}{4} +% \gantttitle{August}{5} +% \gantttitle{September}{3} \\ +% \gantttitlelist{1,...,3}{1} +% \gantttitlelist{1,...,4}{1} +% \gantttitlelist{1,...,5}{1} +% \gantttitlelist{1,...,3}{1} \\ +% \ganttbar[exed]{Requirements \\ Specification}{1}{1} \\ +% \ganttbar[real]{}{1}{2} \\ +% \ganttbar[exed]{Analysis}{2}{3} \\ +% \ganttbar[real]{}{3}{4} \\ +% \ganttbar[exed]{Design}{4}{5} \\ +% \ganttbar[real]{}{5}{7} \\ +% \ganttbar[exed]{Coding and \\ Testing}{6}{11} \\ +% \ganttbar[real]{}{8}{12} \\ +% \ganttbar[exed]{Implementation}{12}{13} \\ +% \ganttbar[real]{}{13}{15} \\ +% \end{ganttchart} +% \end{center} +% \vfill + +\fi % UML Diagrams + + +\iftrue % System Implementation + + +\section{System Implementation} + +\lstset{language=Go} +\lstset{basicstyle=\ttffamily} + +\subsubsection{server.go} +\lstinputlisting{../src/server.go}% + +\subsubsection{helpers.go} +\lstinputlisting{../src/helpers.go} + +\subsubsection{files.go} +\lstinputlisting{../src/files.go} + +\subsubsection{templates.go} +\lstinputlisting{../src/templates.go} + +\lstset{language=Bash} + +\subsubsection{automountd} +\lstinputlisting{../scripts/automountd} + +\subsubsection{automount-clean} +\lstinputlisting{../scripts/automount-clean} + + +\fi % System Implementation + + +\fontsize{12pt}{24pt}\selectfont + +\iftrue % Miscellaneous + +\section{Results} + +Connecting to Pi and loading the page asks for credentials. \\ \\ +\includegraphics[width=\linewidth]{ss/1.cm-login.png} \\ + +The home page will be loaded when the right credentials are entered. It contains a list of drives that are currentyl mounted to any of the Rasberry Pi USB Ports. \\ \\ +\includegraphics[width=\linewidth]{ss/2.cm-home.png} \\ + +Clicking on the upload button opens a dialog which the user can use to select and upload multiple files. \\ \\ +\includegraphics[width=\linewidth]{ss/3.cm-upload.png} \\ + +Clicking on the new folder button in lower right corner opens up a prompt that inputs a name and create a new folder. \\ \\ +\includegraphics[width=\linewidth]{ss/4.cm-create-folder.png} \\ + +\clearpage +The user can also select multiple files and perform various file operations on selected files. \\ \\ +\includegraphics[width=\linewidth]{ss/5.cm-select.png} \\ + +Here's an example of user cutting a bunch of files. \\ \\ +\includegraphics[width=\linewidth]{ss/6.cm-cut.png} \\ + +\clearpage +Same with copying files. \\ \\ +\includegraphics[width=\linewidth]{ss/7.cm-cut-copy.png} \\ + +The clear button will cancel any cut/copy operations where as the paste button will paste all the files from cut/copy buffer in the directory the user is currently in. Similary the user can also downlad files using file selection. \\ \\ + +When deleting files, the user is prompted with a confirmation dialog box with count of files that are goint to be deleted. \\ \\ +\includegraphics[width=\linewidth]{ss/8.cm-delete-prompt.png} + + +\section{Future Scope and Conclusion} +\vspace{2cm} +\quad\quad +The \inserttitle{} system proves to be a very useful tool for digital professionals and even normal users. However, this was really a minimal working demonstration of can be done with a cheap computer (like Rasberry Pi) and bunch of media devices. A lot more can be done to extend the current systems as well as make it do things that it was not intended to. \\ +\par +It's authentication system can be improved to include permanent logins and session managements using browser cookies. At the movement, it only supports HTTP BasicAuth and user has to login everytime they open a new browser instance. \\ +\par +This system can include automatic backup and sync functionality with native applications interfacing the web server. This would be the key feature for driving mass adoption by users as one of the primary view of cloud storage is of a 'backup point.' Users will also don't have to bother with manuall uploading or downloading files as they can be automatically synced in the background. \\ +\par +A public sharing functionality can be introduced where a user selects a folder or a bunch of files and a link is automatically generated that allows other people or non-users of the system to access those files. This will also replace the reliance on third party file sharing sites like Zapya or ToffeeShare. + + +\section{References} +\vspace{2cm} +\begin{enumerate} + \item Go Language Specificatoin: \quad \url{https://go.dev/ref/spec} + \item Go Package Documentation: \quad \url{https://pkg.go.dev/std} + \item Go User Guides: \quad \url{https://go.dev/doc/} + \item Basic Auth: \\ \quad \url{https://www.alexedwards.net/blog/basic-authentication-in-go} + \item Stack Overflow: \quad \url{https://stackoverflow.com} + \item Google Drive Pricing: \quad \url{https://one.google.com/about/plans} + \item Nginx: \quad \url{https://nginx.org/en/} + \item OpenSSH: \quad \url{https://www.openssh.com/} + \item ArchLinux Wiki: \quad \url{https://wiki.archlinux.org/} + \item MDN Docs: \quad \url{https://developer.mozilla.org/en-US/} +\end{enumerate} + + +\fi % Miscellaneous + + +\end{document} + diff --git a/report/images/dupli-checker.png b/report/images/dupli-checker.png new file mode 100644 index 0000000..e67f28a Binary files /dev/null and b/report/images/dupli-checker.png differ diff --git a/report/images/ss/1.cm-login.png b/report/images/ss/1.cm-login.png new file mode 100644 index 0000000..41a8b06 Binary files /dev/null and b/report/images/ss/1.cm-login.png differ diff --git a/report/images/ss/2.cm-home.png b/report/images/ss/2.cm-home.png new file mode 100644 index 0000000..2b3eab1 Binary files /dev/null and b/report/images/ss/2.cm-home.png differ diff --git a/report/images/ss/3.cm-upload.png b/report/images/ss/3.cm-upload.png new file mode 100644 index 0000000..a00df0d Binary files /dev/null and b/report/images/ss/3.cm-upload.png differ diff --git a/report/images/ss/4.cm-create-folder.png b/report/images/ss/4.cm-create-folder.png new file mode 100644 index 0000000..3ce4e2f Binary files /dev/null and b/report/images/ss/4.cm-create-folder.png differ diff --git a/report/images/ss/5.cm-select.png b/report/images/ss/5.cm-select.png new file mode 100644 index 0000000..e461782 Binary files /dev/null and b/report/images/ss/5.cm-select.png differ diff --git a/report/images/ss/6.cm-cut.png b/report/images/ss/6.cm-cut.png new file mode 100644 index 0000000..27ee175 Binary files /dev/null and b/report/images/ss/6.cm-cut.png differ diff --git a/report/images/ss/7.cm-cut-copy.png b/report/images/ss/7.cm-cut-copy.png new file mode 100644 index 0000000..0846913 Binary files /dev/null and b/report/images/ss/7.cm-cut-copy.png differ diff --git a/report/images/ss/8.cm-delete-prompt.png b/report/images/ss/8.cm-delete-prompt.png new file mode 100644 index 0000000..1d9d9c2 Binary files /dev/null and b/report/images/ss/8.cm-delete-prompt.png differ diff --git a/report/images/vartak-logo.jpg b/report/images/vartak-logo.jpg new file mode 100644 index 0000000..67f9d9d Binary files /dev/null and b/report/images/vartak-logo.jpg differ diff --git a/report/images/vartak-logo.svg b/report/images/vartak-logo.svg new file mode 100644 index 0000000..b75578e --- /dev/null +++ b/report/images/vartak-logo.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/scripts/automount-clean b/scripts/automount-clean new file mode 100755 index 0000000..eff3cc9 --- /dev/null +++ b/scripts/automount-clean @@ -0,0 +1,9 @@ +#!/bin/sh + +while sleep 10; do + find "/media/$USER" -mindepth 1 -maxdepth 1 -printf "%P\n" | while read -r mount; do + [ -n "$(lsblk | grep -F "$mount")" ] && continue + sudo fusermount -u "/media/$USER/$mount" + rmdir -v "/media/$USER/$mount" + done +done diff --git a/scripts/automountd b/scripts/automountd new file mode 100755 index 0000000..7276922 --- /dev/null +++ b/scripts/automountd @@ -0,0 +1,19 @@ +#!/bin/sh + +pathtoname() { + udevadm info -p /sys/"$1" | awk -v FS== '/DEVNAME/ {print $2}' +} + +stdbuf -oL -- udevadm monitor --udev -s block | while read -r -- _ _ event devpath _; do + if [ "$event" = add ]; then + devname=$(pathtoname "$devpath") + # udisksctl mount --block-device "$devname" --no-user-interaction + + target="$(lsblk -no LABEL "$devname")" + [ -z "$target" ] && target="$(lsblk -no UUID "$devname")" + [ -z "$target" ] && continue + sudo mount -v --mkdir "$devname" "/media/root/$target" + mkdir -pv "/media/master/$target" + sudo bindfs -u $(id -u) -g $(id -g) "/media/root/$target" "/media/master/$target" + fi +done diff --git a/src/copy.go b/src/copy.go new file mode 100644 index 0000000..1076d41 --- /dev/null +++ b/src/copy.go @@ -0,0 +1,124 @@ + +// Package gorecurcopy provides recursive copying in Go (golang) with a +// minimum of extra packages. Original concept by Oleg Neumyvakin +// (https://stackoverflow.com/users/1592008/oleg-neumyvakin) and modified +// by Dirk Avery. +// Slightly modified by me. +package main + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + // "syscall" +) + +// copyDir recursively copies a src directory to a destination. +func copyDir(src, dst string) error { + entries, err := ioutil.ReadDir(src) + if err != nil { + return err + } + for _, entry := range entries { + sourcePath := filepath.Join(src, entry.Name()) + destPath := filepath.Join(dst, entry.Name()) + + fileInfo, err := os.Lstat(sourcePath) + if err != nil { + return err + } + + switch fileInfo.Mode() & os.ModeType { + case os.ModeDir: + if err := _createDir(destPath, 0755); err != nil { + return err + } + if err := copyDir(sourcePath, destPath); err != nil { + return err + } + case os.ModeSymlink: + if err := copySymlink(sourcePath, destPath); err != nil { + return err + } + default: + if err := _copy(sourcePath, destPath); err != nil { + return err + } + } + + /* + stat, ok := fileInfo.Sys().(*syscall.Stat_t) + if !ok { + return fmt.Errorf("failed to get raw syscall.Stat_t data for '%s'", sourcePath) + } + if err := os.Lchown(destPath, int(stat.Uid), int(stat.Gid)); err != nil { + return err + } + */ + + isSymlink := entry.Mode()&os.ModeSymlink != 0 + if !isSymlink { + if err := os.Chmod(destPath, entry.Mode()); err != nil { + return err + } + } + } + return nil +} + +// Copy copies a src file to a dst file where src and dst are regular files. +func _copy(src, dst string) error { + sourceFileStat, err := os.Stat(src) + if err != nil { + return err + } + + if !sourceFileStat.Mode().IsRegular() { + return fmt.Errorf("%s is not a regular file", src) + } + + source, err := os.Open(src) + if err != nil { + return err + } + defer source.Close() + + destination, err := os.Create(dst) + if err != nil { + return err + } + defer destination.Close() + _, err = io.Copy(destination, source) + return err +} + +func _exists(path string) bool { + if _, err := os.Stat(path); os.IsNotExist(err) { + return false + } + + return true +} + +func _createDir(dir string, perm os.FileMode) error { + if _exists(dir) { + return nil + } + + if err := os.MkdirAll(dir, perm); err != nil { + return fmt.Errorf("failed to create directory: '%s', error: '%s'", dir, err.Error()) + } + + return nil +} + +// copySymlink copies a symbolic link from src to dst. +func copySymlink(src, dst string) error { + link, err := os.Readlink(src) + if err != nil { + return err + } + return os.Symlink(link, dst) +} diff --git a/src/files.go b/src/files.go new file mode 100644 index 0000000..831ed4d --- /dev/null +++ b/src/files.go @@ -0,0 +1,449 @@ +package main + +import ( + "archive/zip" + "bufio" + "io" + "os" + "fmt" + "html/template" + "net/http" + "path/filepath" + "sort" + "strconv" + "strings" + // "syscall" +) + +type MalformedLinkError struct { + Link string + Target string +} + +func (e *MalformedLinkError) Error() string { return fmt.Sprintf("%s: broken link to %s", e.Link, e.Target) } + +type FileNode struct { + URI string + Path string + IsDir bool + Info os.FileInfo + Data any +} + +func (fileNode *FileNode) HTMLPath() template.HTML { + var htmlpath string + htmlpath += `` + "Home" + ` ` + p := strings.Split(fileNode.URI, string(os.PathSeparator)) + for i, dir := range p { + if p[i] != "" { + htmlpath += `> ` + dir + ` ` + } + } + return template.HTML(htmlpath) +} + +func (fileNode *FileNode) EvalSymlinks() (string, *FileNode, error) { + var err error + target, path, err := linkDeref(fileNode.Path); + if err != nil { + if os.IsNotExist(err) { + return "", nil, err + } + return target, nil, err + } + fileInfo, err := os.Stat(path) + if err != nil { + return target, nil, err + } + return target, &FileNode{ + Path: path, + URI: strings.TrimPrefix(path, homeDir), + Info: fileInfo, + IsDir: fileInfo.IsDir(), + }, nil +} + +func (fileNode *FileNode) IconPath() (string, error) { + var icon string; + switch fileNode.Info.Mode() & os.ModeType { + default: icon = "file-earmark.svg" + case os.ModeIrregular: icon = "question.svg" + case os.ModeDir: icon = "folder2.svg" + case os.ModeSymlink: + _, fileNode, err := fileNode.EvalSymlinks() + if err != nil { + if !os.IsNotExist(err) { + return "", err + } + icon = "link-broken-45deg.svg" + } else { + if fileNode.IsDir { + icon = "folder-symlink.svg" + } else { + icon = "link-45deg.svg" + } + } + } + return filepath.Join("/static/icons/bs/files", icon), nil +} + +func (fileNode *FileNode) Size() (string, error) { + var err error + if fileNode.Mode() == "l" { + _, fileNode, err = fileNode.EvalSymlinks() + if err != nil { + return "", nil + } + } + if fileNode.IsDir { + return "", nil + } + size := float64(fileNode.Info.Size()) + if size < 100 { + return strconv.FormatFloat(size, 'f', 0, 64) + " B", nil + } + units := []string{" KB", " MB", " GB", " TB", " PB", " EB", " ZB"} + for i := 0; i < 7; i++ { + size /= 1024 + if size < 100 { + return strconv.FormatFloat(size, 'f', 1, 64) + units[i], nil + } + } + return strconv.FormatFloat(size, 'f', 1, 64) + " YiB", nil +} + +func (fileNode *FileNode) Mode() string { + switch fileNode.Info.Mode() & os.ModeType { + default: return "f" + case os.ModeDir: return "d" + case os.ModeSymlink: return "l" + } +} + +func (fileNode *FileNode) ModDate() string { + t := fileNode.Info.ModTime() + return fmt.Sprintf("%.3s %d, %d\n", t.Month(), t.Day(), t.Year()) +} + +func (fileNode *FileNode) ModTime() string { + t := fileNode.Info.ModTime() + return fmt.Sprintf("%d:%d\n", t.Hour(), t.Minute()) +} + + +func (fileNode *FileNode) Details() (string, error) { + text := "Non-Regular File" + switch fileNode.Info.Mode() & os.ModeType { + + default: + if fileNode.Info.Size() == 0 { + return "Empty File", nil + } + file, err := os.Open(fileNode.Path) + if err != nil { + return "", err + } + buffer := make([]byte, 512) + _, err = file.Read(buffer) + if err != nil { + return "", err + } + contentType := http.DetectContentType(buffer) + if contentType == "application/octet-stream" { + text = "Text File" + } else { + text = "*"+contentType + } + + case os.ModeDir: + text = "Folder" + + case os.ModeSymlink: + target, _, err := fileNode.EvalSymlinks() + if err != nil { + if !os.IsNotExist(err) { + return "", err + } + if len(target) > 0 { + text = "Broken Link to '"+target+"'" + } else { + text = "Inaccessible Link" + } + } else { + text = "Link to " + target + } + + case os.ModeSocket: + text = "Unix Socket" + + case os.ModeDevice: + text = "Device File" + + case os.ModeNamedPipe: + text = "Named Pipe" + + case os.ModeTemporary: + text = "Temporary File" + + case os.ModeAppend: + case os.ModeExclusive: + case os.ModeSetuid: + case os.ModeSetgid: + case os.ModeCharDevice: + case os.ModeSticky: + case os.ModeIrregular: + } + + return text, nil +} + + +func getDirSize(path string) (int64, error) { + var size int64 + err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + size += info.Size() + } + return err + }) + return size, err +} + + +func getDirList(path string, sortBy string, ascending bool, dirsFirst bool) ([]*FileNode, error) { + entries, err := os.ReadDir(path) + if err != nil { + return nil, err + } + files := make([]*FileNode, len(entries)) + for i, entry := range entries { + filePath := filepath.Join(path, entry.Name()) + fileURI := strings.TrimLeft(path, homeDir) + fileInfo, err := entry.Info() + if err != nil { + return nil, err + } + files[i] = &FileNode{ + Path: filePath, + URI: fileURI, + IsDir: entry.IsDir(), + Info: fileInfo, + } + } + + switch sortBy { + case "name": sort.SliceStable(files, func(i, j int) bool { + return strings.ToLower(files[i].Info.Name()) < strings.ToLower(files[j].Info.Name()) + }) + case "size": sort.SliceStable(files, func(i, j int) bool { + return files[i].Info.Size() < files[j].Info.Size() + }) + case "time": sort.SliceStable(files, func(i, j int) bool { + return files[i].Info.ModTime().Before(files[j].Info.ModTime()) + }) + } + + if !ascending { + for i, j := 0, len(files)-1; i < j; i, j = i+1, j-1 { + files[i], files[j] = files[j], files[i] + } + } + + if dirsFirst { + var dirs, notDirs []*FileNode + for _, fileNode := range files { + info, err := os.Stat(fileNode.Path) + if err != nil { + if os.IsNotExist(err) { + info, err = os.Lstat(fileNode.Path) + if err != nil { + return nil, err + } + } else { + return nil, err + } + } + if info.IsDir() { + dirs = append(dirs, fileNode) + } else { + notDirs = append(notDirs, fileNode) + } + } + return append(dirs, notDirs...), nil + } + + return files, nil +} + + +func addToZip(source string, writer *zip.Writer) error { + return filepath.Walk(source, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + header.Method = zip.Deflate + header.Name, err = filepath.Rel(filepath.Dir(source), path) + if err != nil { + return err + } + if info.IsDir() { + header.Name += "/" + } + headerWriter, err := writer.CreateHeader(header) + if err != nil { + return err + } + if !info.Mode().IsRegular() { + return nil + } + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + + _, err = io.Copy(headerWriter, f) + return err + }) +} + + +func readBuffer(path string) ([]string, error) { + buff, err := os.OpenFile(path, os.O_RDONLY|os.O_CREATE, 0600) + if err != nil { + return nil, err + } + defer buff.Close() + + var buffer []string + scanner := bufio.NewScanner(buff) + for scanner.Scan() { + buffer = append(buffer, scanner.Text()) + } + return buffer, nil +} + + +func fileExists(path string) (bool, error) { + _, err := os.Lstat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} + + +func copyFile(src, dst string) error { + fin, err := os.Open(src) + if err != nil { + return err + } + defer fin.Close() + + fout, err := os.Create(dst) + if err != nil { + return err + } + defer fout.Close() + + _, err = io.Copy(fout, fin) + if err != nil { + return err + } + fin.Close() + + return nil +} + + +func copyTo(src, dstDir string) error { + info, err := os.Lstat(src) + if err != nil { + return err + } + dst := filepath.Join(dstDir, info.Name()) + + fmt.Printf("Copying %s to %s\n", src, dstDir) + switch info.Mode() & os.ModeType { + case os.ModeDir: + if err := os.MkdirAll(dst, 0755); err != nil { + return err + } + if err := copyDir(src, dst); err != nil { + return err + } + case os.ModeSymlink: + if err := copySymlink(src, dst); err != nil { + return err + } + default: + if err := copyFile(src, dst); err != nil { + return err + } + } + fmt.Println("Finished Copying.\n\n") + + if info.Mode()&os.ModeSymlink == 0 { + return os.Chmod(dst, info.Mode()) + } + return nil +} + + +func linkDeref(link string) (string, string, error) { + target, err := os.Readlink(link) + if err != nil { + return "", "", err + } + path := target + if filepath.IsAbs(target) { + if !strings.HasPrefix(path, homeDir) { + return target, "", os.ErrNotExist + } + target = strings.TrimPrefix(target, homeDir) + } else { + path = filepath.Join(filepath.Dir(link), path) + if !strings.HasPrefix(path, homeDir) { + return target, "", os.ErrNotExist + } + } + return target, path, nil +} + + +func readData(key string) ([]byte, error) { + data, err := os.ReadFile(filepath.Join(dataDir, key)) + if err != nil { + return nil, err + } + return data, nil +} + + +func writeData(key string, data []byte) error { + return os.WriteFile(filepath.Join(dataDir, key), data, 644) +} + + +var dataDir string + +func init() { + userHome, err := os.UserHomeDir() + if err != nil { + panic(err) + } + dataDir = filepath.Join(userHome, ".local/share/cloud-maker") + if err = os.MkdirAll(dataDir, 0755); err != nil { + panic(err) + } +} diff --git a/src/go.mod b/src/go.mod new file mode 100644 index 0000000..106b070 --- /dev/null +++ b/src/go.mod @@ -0,0 +1,5 @@ +module cloud-maker/server + +go 1.23 + +require golang.org/x/crypto v0.28.0 // indirect diff --git a/src/go.sum b/src/go.sum new file mode 100644 index 0000000..29e53ba --- /dev/null +++ b/src/go.sum @@ -0,0 +1,2 @@ +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= diff --git a/src/helpers.go b/src/helpers.go new file mode 100644 index 0000000..c544d71 --- /dev/null +++ b/src/helpers.go @@ -0,0 +1,292 @@ +package main + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "os" + "os/user" + "path/filepath" + "regexp" + "strings" +) + +type ServerError struct { + Err error + Message string + Status int +} + +func (e *ServerError) Error() string { return e.Err.Error() } +func (e *ServerError) Unwrap() error { return e.Err } + + +func getFileNode(URL string) (*FileNode, *ServerError) { + path, err := url.PathUnescape(URL) + if err != nil { + return nil, &ServerError{err, "", 500} + } + p := strings.Split(path, "/") + fileURI := strings.Trim(strings.Join(p[2:], "/"), "/") + filePath := filepath.Join(homeDir, fileURI) + + fileInfo, err := os.Lstat(filePath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, &ServerError{err, fileURI+" not found", 404} + } + return nil, &ServerError{err, "", 500} + } + + return &FileNode{ + Path: filePath, + URI: fileURI, + IsDir: fileInfo.IsDir(), + Info: fileInfo, + }, nil +} + + +var filePattern = regexp.MustCompile(`^-file-entry--(.+)$`) + +func getSelectedNodes(r *http.Request) (*FileNode, []*FileNode, *ServerError) { + fileNode, e := getFileNode(r.URL.Path) + if e != nil { + return nil, nil, e + } + r.ParseMultipartForm(65536) + fmt.Println(r.Form) + var fileNames []string + for key := range r.Form { + if match := filePattern.FindStringSubmatch(key); len(match) > 1 { + fileNames = append(fileNames, match[1]) + } + } + fmt.Printf("FileNames: %s\n", fileNames) + if len(fileNames) == 0 { + return fileNode, []*FileNode{fileNode}, nil + } + + files := make([]*FileNode, len(fileNames)) + for i, fileName := range fileNames { + fileNode, e := getFileNode(filepath.Join(r.URL.Path, fileName)) + if e != nil { + return fileNode, nil, e + } + files[i] = fileNode + } + return fileNode, files, nil +} + + +func sendFile(w http.ResponseWriter, r *http.Request, info ...string) { + fmt.Printf("info: %s\n", info[:]) + if len(info) < 2 { + info = append(info, filepath.Base(info[0])) + } + w.Header().Set("Content-Disposition", "attachment; filename=" + info[1]) + http.ServeFile(w, r, info[0]) +} + + +func addSelectionToBuffer(w http.ResponseWriter, r *http.Request, bufferPath string) *ServerError { + _, files, e := getSelectedNodes(r) + if e != nil { + return e + } + buffer, err := readBuffer(bufferPath) + if err != nil { + return &ServerError{err, "", 500} + } + buff, err := os.OpenFile(bufferPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return &ServerError{err, "", 500} + } + var fileURI string + + writeToBuffer: + for _, file := range files { + fileURI = strings.Trim(file.URI, "/") + for _, line := range buffer { + if strings.Trim(line, "/") == fileURI { + continue writeToBuffer + } + } + if file.IsDir { + fileURI += "/" + } + buff.WriteString("/" + fileURI + "\r\n") + } + + http.Redirect(w, r, r.URL.Path, 303) + return nil +} + + +func deleteBuffer(w http.ResponseWriter, r *http.Request, bufferPath string) *ServerError { + err := os.Remove(bufferPath) + if err != nil { + return &ServerError{err, "", 500} + } + http.Redirect(w, r, r.URL.Path, 303) + return nil +} + + +func moveFilesFromBuffer(w http.ResponseWriter, r *http.Request, bufferPath string) *ServerError { + fileNode, serr := getFileNode(r.URL.Path) + if serr != nil { + return serr + } + if !fileNode.IsDir { + return &ServerError{nil, "Cannot move file, destination is not a directory", 400} + } + buffer, err := readBuffer(bufferPath) + if err != nil { + return &ServerError{err, "", 500} + } + for _, line := range buffer { + err := copyTo(filepath.Join(homeDir, line), fileNode.Path) + if err != nil { + return &ServerError{err, "", 500} + } + } + deleteBuffer(w, r, bufferPath) + return nil +} + + +func pasteFilesFromBuffer(w http.ResponseWriter, r *http.Request, bufferPath string) *ServerError { + fileNode, serr := getFileNode(r.URL.Path) + if serr != nil { + return serr + } + if !fileNode.IsDir { + return &ServerError{nil, "Cannot copy files, destination is not a directory", 400} + } + buffer, err := readBuffer(bufferPath) + if err != nil { + return &ServerError{err, "", 500} + } + fmt.Printf("Buffer content: %s\n", buffer) + for _, line := range buffer { + err := copyTo(filepath.Join(homeDir, line), fileNode.Path) + if err != nil { + return &ServerError{err, "", 500} + } + } + deleteBuffer(w, r, bufferPath) + return nil +} + + +func createNewDirectory(w http.ResponseWriter, r *http.Request) *ServerError { + fileNode, serr := getFileNode(r.URL.Path) + if serr != nil { + return serr + } + dirname := strings.TrimSpace(r.FormValue("newdir")) + msg := "Requested to create directory '"+dirname+"' in "+fileNode.URI+"\n" + if dirname == "" { + msg = "Directory name cannot be empty.\n" + msg + return &ServerError{nil, msg, 400} + } + if !fileNode.IsDir { + msg = "Cannot create directory, the given destination is a file.\n" + msg + return &ServerError{nil, msg, 400} + } + path := filepath.Join(fileNode.Path, dirname) + isExist, err := fileExists(path) + if isExist { + msg = "Cannot create directory, a file with given name already exists.\n" + msg + return &ServerError{nil, msg, 400} + } + err = os.Mkdir(path, 0755) + if err != nil { + return &ServerError{err, "", 500} + } + http.Redirect(w, r, r.URL.Path, 303) + return nil +} + + +func blockAction(w http.ResponseWriter, r *http.Request, action string) *ServerError { + msg := "" + _, files, serr := getSelectedNodes(r) + if serr != nil { + return serr + } + msg += "The " + action + " operation is currently disabled for testing and security reasons.\n" + msg += "You requested to " + action + " following files :-\n\n" + for _, file := range files { + msg += file.Path + "\n" + } + fmt.Fprintf(w, msg) + return nil +} + + +func deleteSelectedFiles(w http.ResponseWriter, r *http.Request) *ServerError { + // return blockAction(w, r, "delete") + fileNode, files, serr := getSelectedNodes(r) + if serr != nil { + return serr + } + for _, file := range files { + if file.Path == homeDir { + return &ServerError{nil, "Cannot delete root directory.", 400} + } + fmt.Printf("Deleting: %s\n", file.Path) + err := os.RemoveAll(file.Path) + if err != nil { + return &ServerError{err, "", 500} + } + fmt.Println("Deleted.") + } + isExist, err := fileExists(fileNode.Path) + if err != nil { + return &ServerError{err, "", 500} + } + if !isExist { + http.Redirect(w, r, "/view/" + filepath.Dir(fileNode.URI), 303) + } else { + http.Redirect(w, r, "/view/" + fileNode.URI, 303) + } + return nil +} + + +var ( + homeDir = "/media/" + tempDir = "/tmp/cloud/" + cutBuffer = filepath.Join(tempDir, "cut_buffer") + copyBuffer = filepath.Join(tempDir, "copy_buffer") +) + + +func init() { + err := os.MkdirAll(tempDir, 0755); + if err != nil { + panic(err) + } + mountpoint := os.Getenv("CLOUD_MAKER_HOME") + if mountpoint != "" { + isExist, err := fileExists(mountpoint) + if err != nil { + panic(err) + } + if isExist { + homeDir = mountpoint + } + return + } + + u, err := user.Current() + if err != nil { + panic(err) + } + homeDir += u.Username +} + + diff --git a/src/server.go b/src/server.go new file mode 100644 index 0000000..16df50f --- /dev/null +++ b/src/server.go @@ -0,0 +1,298 @@ +package main + +import ( + "archive/zip" + "bytes" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "reflect" + "strings" + + "golang.org/x/crypto/bcrypt" +) + +func processAction(w http.ResponseWriter, r *http.Request, action string) *ServerError { + switch action { + default: + return &ServerError{nil, "Invalid Action", 400} + case "cut": + return addSelectionToBuffer(w, r, cutBuffer) + case "copy": + return addSelectionToBuffer(w, r, copyBuffer) + case "cancel-cut": + return deleteBuffer(w, r, cutBuffer) + case "cancel-copy": + return deleteBuffer(w, r, copyBuffer) + case "cut-paste": + return moveFilesFromBuffer(w, r, cutBuffer) + case "copy-paste": + return pasteFilesFromBuffer(w, r, copyBuffer) + case "newdir": + return createNewDirectory(w, r) + case "delete": + return deleteSelectedFiles(w, r) + } +} + +func viewHandler(w http.ResponseWriter, r *http.Request) *ServerError { + var err error + var serr *ServerError + for k, v := range r.URL.Query() { + switch k { + default: + http.Redirect(w, r, r.URL.Path, 302) + case "action": + return processAction(w, r, v[0]) + } + } + fileNode, serr := getFileNode(r.URL.Path) + if serr != nil { + return serr + } + if fileNode.Info.Mode()&os.ModeSymlink != 0 { + fileURI := fileNode.URI + target := "" + target, fileNode, err = fileNode.EvalSymlinks() + if err != nil { + if !os.IsNotExist(err) { + return &ServerError{err, "", 500} + } + if len(target) != 0 { + return &ServerError{err, fileURI + ": broken link to '" + target + "'", 404} + } else { + return &ServerError{err, fileURI + ": Inaccessible link", 404} + } + } + } + if !fileNode.IsDir { + http.ServeFile(w, r, fileNode.Path) + return nil + } + dirList, err := getDirList(fileNode.Path, "name", true, true) + if err != nil { + return &ServerError{err, "", 404} + } + + fileNode.Data = dirList + fmt.Printf("View: '%s'\n", fileNode.URI) + if strings.TrimSpace(fileNode.URI) == "" { + return renderTemplate(w, "viewHome", &FSData{ + FileCount: len(dirList), + File: fileNode, + }) + } + + cutBuf, err := readBuffer(cutBuffer) + if err != nil { + return &ServerError{err, "", 500} + } + copyBuf, err := readBuffer(copyBuffer) + if err != nil { + return &ServerError{err, "", 500} + } + return renderTemplate(w, "viewDir", &FSData{ + CutCount: len(cutBuf), + CutBuffer: cutBuf, + CopyCount: len(copyBuf), + CopyBuffer: copyBuf, + FileCount: len(dirList), + File: fileNode, + }) +} + +func profileHandler(w http.ResponseWriter, r *http.Request) *ServerError { + username, err := readData("username") + if err != nil { + return &ServerError{err, "", 500} + } + + if r.Method == "GET" { + return renderTemplate(w, "profile", &Profile{ + Username: string(username), + }) + } + + newUsername := strings.TrimSpace(r.FormValue("username")) + if newUsername == "" { + return renderTemplate(w, "profile", &Profile{ + Message: "Username cannot be empty.", + Status: "warning", + }) + } + if newUsername != string(username) { + if err := writeData("username", []byte(newUsername)); err != nil { + return &ServerError{err, "", 500} + } + } + + newPassword := r.FormValue("password") + confirmPassword := r.FormValue("confirm-pass") + if newPassword != "" { + if newPassword != confirmPassword { + return renderTemplate(w, "profile", &Profile{ + Username: string(username), + Message: "Passwords do not match.", + Status: "warning", + }) + } + newPassHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + return &ServerError{err, "", 500} + } + writeData("password", newPassHash) + } + + return renderTemplate(w, "profile", &Profile{ + Username: string(username), + Message: "Profile updated successfully.", + Status: "success", + }) +} + +func downloadHandler(w http.ResponseWriter, r *http.Request) *ServerError { + fmt.Printf("%s\n", r.Form) + fileNode, files, serr := getSelectedNodes(r) + if serr != nil { + return serr + } + if len(files) == 1 && !files[0].IsDir { + sendFile(w, r, files[0].Path) + return nil + } + zipName := fileNode.Info.Name() + ".zip" + target := "/tmp/cloud/" + zipName + + archive, err := os.Create(target) + if err != nil { + return &ServerError{err, "", 500} + } + defer archive.Close() + + zipWriter := zip.NewWriter(archive) + defer zipWriter.Close() + + for _, file := range files { + err := addToZip(file.Path, zipWriter) + if err != nil { + return &ServerError{err, "", 500} + } + } + zipWriter.Close() + sendFile(w, r, target, zipName) + return nil +} + +func uploadHandler(w http.ResponseWriter, r *http.Request) *ServerError { + fileNode, serr := getFileNode(r.URL.Path) + if serr != nil { + return serr + } + r.ParseMultipartForm(65536) + formData := r.MultipartForm + + for _, handler := range formData.File["attachments"] { + fmt.Printf("%v\n", handler.Header) + fmt.Println(handler.Filename, ":", handler.Size) + file, err := handler.Open() + if err != nil { + return &ServerError{err, "", 500} + } + defer file.Close() + filepath := filepath.Join(fileNode.Path, handler.Filename) + fmt.Printf("Saving to %v...", filepath) + f, err := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + return &ServerError{err, "", 500} + } + defer f.Close() + io.Copy(f, file) + fmt.Println("Saved.") + } + http.Redirect(w, r, "/view/"+fileNode.URI, 303) + return nil +} + +func fileHandler(w http.ResponseWriter, r *http.Request) *ServerError { + fileNode, serr := getFileNode(r.URL.Path) + if serr != nil { + return serr + } + if fileNode.IsDir { + return &ServerError{nil, "File not Found.", 404} + } + http.ServeFile(w, r, fileNode.Path) + return nil +} + +func handler(w http.ResponseWriter, r *http.Request) *ServerError { + if r.URL.Path != "/" { + return &ServerError{nil, "Invalid URL", 404} + } + http.Redirect(w, r, "view", 303) + return nil +} + +type httpHandler func(http.ResponseWriter, *http.Request) *ServerError + +func (fn httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + username, password, ok := r.BasicAuth() + if !ok { + w.Header().Add("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) + http.Error(w, "Basic Auth Missing.", 401) + return + } + + realUsername, err := readData("username") + if err != nil { + http.Error(w, "Couldn't retreive username from server.", 500) + } + realPassword, err := readData("password") + if err != nil { + http.Error(w, "Couldn't retreive password from server.", 500) + } + realUsername = bytes.TrimRight(realUsername, "\n") + realPassword = bytes.TrimRight(realPassword, "\n") + + // fmt.Printf("Given credentials: %s:%s\n", username, password) + // fmt.Printf("Requred credentials: %s:%s\n", realUsername, realPassword) + + if string(realUsername) != username { + w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) + http.Error(w, "Username not matched.", http.StatusUnauthorized) + return + } + + if err := bcrypt.CompareHashAndPassword(realPassword, []byte(password)); err != nil { + w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) + http.Error(w, "Authentication Error", http.StatusUnauthorized) + return + } + + if serr := fn(w, r); serr != nil { + if serr.Err != nil { + fmt.Println("\n\nError Type:", reflect.TypeOf(serr.Err)) + fmt.Println("Error Message:", serr.Error()) + } + if serr.Message == "" { + serr.Message = "Internal Server Error" + } + http.Error(w, serr.Message, serr.Status) + } +} + +func main() { + fileServer := http.FileServer(http.Dir("./static")) + http.Handle("/", httpHandler(handler)) + http.Handle("/view/", httpHandler(viewHandler)) + http.Handle("/profile/", httpHandler(profileHandler)) + http.Handle("/upload/", httpHandler(uploadHandler)) + http.Handle("/download/", httpHandler(downloadHandler)) + http.Handle("/file/", httpHandler(fileHandler)) + http.Handle("/static/", http.StripPrefix("/static/", fileServer)) + fmt.Println("\nServer Listening on :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/src/static/icons/bs/actions/arrow-left-short.svg b/src/static/icons/bs/actions/arrow-left-short.svg new file mode 100644 index 0000000..abb15dd --- /dev/null +++ b/src/static/icons/bs/actions/arrow-left-short.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/static/icons/bs/actions/arrow-left.svg b/src/static/icons/bs/actions/arrow-left.svg new file mode 100644 index 0000000..587d4fe --- /dev/null +++ b/src/static/icons/bs/actions/arrow-left.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/static/icons/bs/actions/arrow-right-short.svg b/src/static/icons/bs/actions/arrow-right-short.svg new file mode 100644 index 0000000..fa238ff --- /dev/null +++ b/src/static/icons/bs/actions/arrow-right-short.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/static/icons/bs/actions/arrow-right.svg b/src/static/icons/bs/actions/arrow-right.svg new file mode 100644 index 0000000..2362904 --- /dev/null +++ b/src/static/icons/bs/actions/arrow-right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/static/icons/bs/actions/backspace.svg b/src/static/icons/bs/actions/backspace.svg new file mode 100644 index 0000000..39b688f --- /dev/null +++ b/src/static/icons/bs/actions/backspace.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/static/icons/bs/actions/check.svg b/src/static/icons/bs/actions/check.svg new file mode 100644 index 0000000..11ab547 --- /dev/null +++ b/src/static/icons/bs/actions/check.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/static/icons/bs/actions/clipboard.svg b/src/static/icons/bs/actions/clipboard.svg new file mode 100644 index 0000000..b92f42a --- /dev/null +++ b/src/static/icons/bs/actions/clipboard.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/static/icons/bs/actions/copy.svg b/src/static/icons/bs/actions/copy.svg new file mode 100644 index 0000000..b590680 --- /dev/null +++ b/src/static/icons/bs/actions/copy.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/static/icons/bs/actions/download.svg b/src/static/icons/bs/actions/download.svg new file mode 100644 index 0000000..90a34a3 --- /dev/null +++ b/src/static/icons/bs/actions/download.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/static/icons/bs/actions/folder-plus.svg b/src/static/icons/bs/actions/folder-plus.svg new file mode 100644 index 0000000..85b5a18 --- /dev/null +++ b/src/static/icons/bs/actions/folder-plus.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/static/icons/bs/actions/folder.svg b/src/static/icons/bs/actions/folder.svg new file mode 100644 index 0000000..a30c452 --- /dev/null +++ b/src/static/icons/bs/actions/folder.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/static/icons/bs/actions/scissors.svg b/src/static/icons/bs/actions/scissors.svg new file mode 100644 index 0000000..2f566e4 --- /dev/null +++ b/src/static/icons/bs/actions/scissors.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/static/icons/bs/actions/trash.svg b/src/static/icons/bs/actions/trash.svg new file mode 100644 index 0000000..3020264 --- /dev/null +++ b/src/static/icons/bs/actions/trash.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/static/icons/bs/actions/upload.svg b/src/static/icons/bs/actions/upload.svg new file mode 100644 index 0000000..9a4a363 --- /dev/null +++ b/src/static/icons/bs/actions/upload.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/static/icons/bs/files/file-earmark.svg b/src/static/icons/bs/files/file-earmark.svg new file mode 100644 index 0000000..eb55426 --- /dev/null +++ b/src/static/icons/bs/files/file-earmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/icons/bs/files/folder-symlink.svg b/src/static/icons/bs/files/folder-symlink.svg new file mode 100644 index 0000000..72fbd2e --- /dev/null +++ b/src/static/icons/bs/files/folder-symlink.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/static/icons/bs/files/folder2.svg b/src/static/icons/bs/files/folder2.svg new file mode 100644 index 0000000..be82283 --- /dev/null +++ b/src/static/icons/bs/files/folder2.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/icons/bs/files/link-45deg.svg b/src/static/icons/bs/files/link-45deg.svg new file mode 100644 index 0000000..fb527fa --- /dev/null +++ b/src/static/icons/bs/files/link-45deg.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/static/icons/bs/files/link-broken-45deg.svg b/src/static/icons/bs/files/link-broken-45deg.svg new file mode 100644 index 0000000..4f20cd3 --- /dev/null +++ b/src/static/icons/bs/files/link-broken-45deg.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/static/icons/bs/files/question.svg b/src/static/icons/bs/files/question.svg new file mode 100644 index 0000000..ba185ad --- /dev/null +++ b/src/static/icons/bs/files/question.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/static/icons/bs/hdd.svg b/src/static/icons/bs/hdd.svg new file mode 100644 index 0000000..46cc4e4 --- /dev/null +++ b/src/static/icons/bs/hdd.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/static/icons/bs/house-door.svg b/src/static/icons/bs/house-door.svg new file mode 100644 index 0000000..1b0a602 --- /dev/null +++ b/src/static/icons/bs/house-door.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/icons/bs/person-circle.svg b/src/static/icons/bs/person-circle.svg new file mode 100644 index 0000000..b2c0c28 --- /dev/null +++ b/src/static/icons/bs/person-circle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/static/script.js b/src/static/script.js new file mode 100644 index 0000000..8c1bf51 --- /dev/null +++ b/src/static/script.js @@ -0,0 +1,41 @@ +"use strict"; +"use warnings"; + +function checkAll(checked, scope) { + let inputs = scope.getElementsByTagName('input'); + for (var i = 0; i < inputs.length; i++) { + if (inputs[i].type.toLowerCase() == 'checkbox') { + inputs[i].checked = checked; + } + } +} + +function toggleAll(checkbox) { + let scope = document.getElementById('file-list') + if (checkbox.checked) { + checkAll(true, scope); + } else { + checkAll(false, scope); + } +} + +function deleteAction(button) { + let table = document.getElementById('file-list') + let tbody = table.getElementsByClassName('tbody')[0] + let inputs = tbody.getElementsByTagName('input') + let msg = 0; + for (var i = 0; i < inputs.length; i++) { + inputs[i].type.toLowerCase() == 'checkbox' && + inputs[i].checked && msg++; + } + if (msg == 0) { + msg = "Whole Folder will be deleted." + } else { + msg += " file(s) selected." + } + if (confirm(msg + " Confirm Deletion?")) { + button.formAction = "?action=delete"; + button.onclick = "submit()"; + button.click(); + } +} diff --git a/src/static/style.css b/src/static/style.css new file mode 100644 index 0000000..b24c197 --- /dev/null +++ b/src/static/style.css @@ -0,0 +1,356 @@ +:root { + --profile-width: 15rem; +} + +body { + background-color: #1b1b1b; + color: lightgrey; + font-family: "Fira Sans"; + margin: auto; + margin-bottom: 10rem; + max-width: 60rem; +} + +header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; +} + +header h1 { + color: orange; +} + +header img { + width: 2rem; + padding: 1rem; +} + +a { + color: #7bf; + text-decoration: none; +} + +button b { + vertical-align: middle; + font-size: 0.8rem; +} + +label b { + font-size: 0.9rem; +} + +button img, details img { + padding: 0.2rem; + width: 1rem; + vertical-align: middle; +} + +#newdir-cb+label { + background-color: #2d60ba; + opacity: 0.6; + position: fixed; + bottom: 10%; + right: 10%; + border: 0; + border-radius: 60%; + padding: 0.6rem 0.85rem; + z-index: 11; +} + +#newdir-cb +label:hover { + opacity: 1; + bottom: 10%; + right: 10%; + padding: 0.8rem 1.05rem; +} + +#newdir-cb:checked +label { + opacity: 0.8; +} + +#newdir-cb+label img { + width: 1.5rem; + margin-top: 0.2rem; +} + +#newdir-bg { + cursor: pointer; + background-color: black; + opacity: 50%; + display: none; + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + bottom: 0; + right: 0; + z-index: 10; +} + +#newdir-box { + color: white; + background: steelblue; + display: none; + position: fixed; + right: 10%; + bottom: 20%; + text-align: center; + width: 15rem; + padding: 1rem; + z-index: 11; +} + +#newdir-box input[type="text"] { + width: 10rem; +} + +#newdir-box button { + width: 2rem; + height: 1.6rem; +} + +#newdir-box img { + height: 1.4rem; + width: 1.6rem; + margin-top: -0.2rem; + margin-left: -0.4rem; +} + +#newdir-box img:hover { + background-color: lightgreen; +} + +#newdir-cb { display: none; } +#newdir-cb:checked ~ #newdir-box { display: block; } +#newdir-cb:checked ~ #newdir-bg { display: block; } + +.form-header label img { + padding: 0.2rem; + width: 1rem; + vertical-align: middle; +} + +.html-path { + padding: 1rem 0rem; + overflow-x: auto; + white-space: nowrap; +} + +.html-path a { + background-color: #333; + border-radius: 0.5rem; + color: #add; + font: 1.5rem; + padding: 0.3rem 0.7rem; + margin: 0.5rem; +} + +.form-actions { + white-space: nowrap; +} + +.clickable::-moz-selection, +.clickable::selection, +.clickable *::-moz-selection, +.clickable *::selection { + background-color: transparent; +} + +.back-button img { + width: 1.8rem; + margin: -0.4rem; +} + +#upload-button + label, +.form-actions button, +.form-actions a { + background-color: #bdd; + color: black; + padding: 0.3rem 0.5rem; + margin-right: 0.5rem; + vertical-align: middle; + border: none; +} + +#upload-button + label:hover, +.form-actions button:hover { + background-color: #8cc; +} + +#upload-button { display: none; } +#upload-button + label { display: inline-block; } +#upload-button:checked + label { background-color: #7dd; } +#upload-button:checked ~ #file-upload-box { display: block; } + +#file-upload-box { + background: steelblue; + display: none; + padding: 0.7rem 1rem; + margin: 1rem 0rem; +} + +#file-upload-box button { + margin-top: -0.25rem; +} + +#file-upload-box { display: none; } + +details { margin: 1rem 0rem; background-color: #aa3333; } +details summary { padding: 0.5rem 1rem; background-color: #cc3333 } +details[open] summary { color: #111; } +details summary span { vertical-align: middle; margin: -0.2rem; } +details .content { padding: 0.5rem 1rem; } +ul { list-style-type: none; } + +.actions { float: right; } + +.table { display: table; } +.thead { display: table-header-group; } +.tbody { display: table-row-group; } +.tr { display: table-row; } +.th, .td { display: table-cell; } + +#file-list { + color: #dddddd; + width: 100%; + margin: 1rem 0rem; + overflow-x: auto; +} +#file-list .thead { background-color: #2b2b2b; } +#file-list .thead:has(input[type=checkbox]:checked) { background-color: #557; } +#file-list .th { font-weight: bold; } +#file-list .tr:hover { background-color: #2b2b2b; } +#file-list .tr:has(input[type=checkbox]:checked) { background-color: #1b2b3b; } +#file-list .tr:has(input[type=checkbox]:checked):hover { background-color: #2b3b4b; } +#file-list .td, #file-list .tr, #file-list .th { + border-bottom: 0.1rem solid #333; + overflow: hidden; + padding: 0.5rem; + font-size: 0.97em; +} +#file-list img { width: 1rem; } +#file-list a { + color: #9cf; + display: block; + margin: -0.8rem; + padding: 1rem; + font-size: 1rem; +} +#file-list a:hover { + text-decoration-line: underline; +} + +.info { + color: lightgray; + font-size: 0.92em; + text-align: center; + padding: 1rem 2rem; +} + +.drives { + padding: 1rem; +} + +.drives a { + background-color: #2b2b2b; + display: flex; + align-items: center; + padding: 0.5em; + margin: 1rem 0rem; +} + +.drives a:hover { + background-color: #1b2b3b; +} + +.drives a span { padding: 0.5rem; } +.drives a img { width: 2rem; } +.drives .desc { color: gray; } + +#profile { + background-color: #2b2b3b; + display: table; + align-items: center; + box-sizing: border-box; + padding: 3rem 5rem; + max-width: var(--profile-width); + margin: auto; + margin-top: 2rem; +} + +#profile h2 { + text-align: center; +} + +#profile label, +#profile input { + height: 1.5rem; + margin: 0.3rem 0rem; + display: block; + width: 100%; +} + +#profile label { + font-weight: bold; + margin: 2rem 0rem 0rem 0rem; +} + +#profile input { + width: var(--profile-width); +} + +#profile button { + color: black; + background-color: #f66; + font-weight: bold; + vertical-align: center; + height: 2rem; + width: 100%; + margin-top: 3rem; + border: none; +} + +#profile button:hover { + background-color: #f88; + cursor: pointer; +} + +#profile p { + max-width: var(--profile-width); +} + +.warning { color: red; } +.success { color: lightgreen; } + + +@media screen and (min-width: 20rem) { + #upload-button+label { float: right; } +} + +@media screen and (max-width: 60rem) { + #file-list .tr .th:nth-child(6), + #file-list .tr .td:nth-child(6) { display: none; } +} + +@media screen and (max-width: 40rem) { + :root { + --profile-width: 10rem; + } + #profile { padding: 2rem 3rem; } + + button b { display: none; } + button { margin-right: 0 !important; } + #file-list .tr .th:nth-child(7), + #file-list .tr .td:nth-child(7) { display: none; } + .hint { display: none; } +} + +@media screen and (max-width: 25rem) { + label b { display: none; } + #file-list .tr .th:nth-child(5), + #file-list .tr .td:nth-child(5) { display: none; } +} + diff --git a/src/templates.go b/src/templates.go new file mode 100644 index 0000000..ed88874 --- /dev/null +++ b/src/templates.go @@ -0,0 +1,45 @@ +package main + +import ( + "net/http" + "html/template" +) + +type FSData struct { + CutCount int + CopyCount int + FileCount int + CutBuffer []string + CopyBuffer []string + File *FileNode +} + +type Profile struct { + Username string + Password string + Message string + Status string +} + +var templates = make(map[string]*template.Template) + +func renderTemplate(w http.ResponseWriter, tmpl string, data any) *ServerError { + if err := templates[tmpl].ExecuteTemplate(w, "base.html", data); err != nil { + return &ServerError{err, "", 500} + } + return nil +} + +func init() { + templates["profile"] = template.Must(template.New( + "viewDir.html", + ).ParseFiles("templates/base.html", "templates/profile.html")) + + templates["viewDir"] = template.Must(template.New( + "viewDir.html", + ).ParseFiles("templates/base.html", "templates/viewDir.html")) + + templates["viewHome"] = template.Must(template.New( + "viewHome.html", + ).ParseFiles("templates/base.html", "templates/viewHome.html")) +} diff --git a/src/templates/base.html b/src/templates/base.html new file mode 100644 index 0000000..98e5787 --- /dev/null +++ b/src/templates/base.html @@ -0,0 +1,17 @@ + + + + + Cloud Maker + + + + + +
+ {{ template "main" . }} +
+ + + + diff --git a/src/templates/profile.html b/src/templates/profile.html new file mode 100644 index 0000000..6e6fccd --- /dev/null +++ b/src/templates/profile.html @@ -0,0 +1,24 @@ +{{ define "main" }} +
+

Cloud Maker

+ +
+ +
+

Profile

+ + + + + + + + + + + + {{ if .Message }} +

{{ .Message }}

+ {{ end }} +
+{{ end }} diff --git a/src/templates/viewDir.html b/src/templates/viewDir.html new file mode 100644 index 0000000..9d45242 --- /dev/null +++ b/src/templates/viewDir.html @@ -0,0 +1,106 @@ +{{ define "main" }} +{{ $location := .File.URI }} +{{ $path := .File.Path }} +
+ +
+

{{ .File.HTMLPath }}

+
+ + + + + + + +
+ + + + +
+
+ + {{ if .CutCount }} +
+ + Files in Cut Buffer ({{ .CutCount }}) + + + + + +
+ {{ range .CutBuffer }}
{{ . }}
{{ end }} +
+
+ {{ end }} + + {{ if .CopyCount }} +
+ + Files in Copy Buffer ({{ .CopyCount }}) + + + + + +
+ {{ range .CopyBuffer }}
{{ . }}
{{ end }} +
+
+ {{ end }} +
+ +
+
+ +
+
+ {{ range .File.Data }} + {{ $name := .Info.Name }} + + {{ end }} +
+
+ + {{ if .FileCount }} +

Hint: aestrik (*) indicates mime-type (media type) of file in Info section.

+ {{ else }} +

Empty Folder

+ {{ end }} +
+ +
+ + +
+
+

Create New Folder

+

+ + +

+
+
+{{ end }} diff --git a/src/templates/viewHome.html b/src/templates/viewHome.html new file mode 100644 index 0000000..bd841d2 --- /dev/null +++ b/src/templates/viewHome.html @@ -0,0 +1,29 @@ +{{ define "main" }} +{{ $location := .File.URI }} +{{ $path := .File.Path }} +
+

Cloud Maker

+ +
+ +
+ +
+ {{ range .File.Data }} + {{ $name := .Info.Name }} + + + +
{{ $name }}
+
drive
+
+
+ {{ end }} +
+ + {{ if not .FileCount }} +

No Drives Mounted.

+ {{ end }} + +
+{{ end }} -- cgit v1.2.3