added admin panel with stats and logs

This commit is contained in:
Chris
2025-05-20 13:59:42 +02:00
parent c3c34efa13
commit 9beddcfd16
16 changed files with 493 additions and 354 deletions

35
DEV.md Normal file
View File

@@ -0,0 +1,35 @@
# Folder structure
All uploads will be created as folders with the name of the file in the `./data` directory.
```
data/
data/<file_name>/
data/<file_name>/<file_name> # the actual file eg abc1
data/<file_name>/meta.json # file containing the metadata
```
## Metadata file
The metadata file will contain data like in the following example:
```json
{
"mime": "image\/jpeg",
"size": 504921,
"size_human": "493.09 KB",
"original_filename": "PXL_20250511_122019042-POP_OUT.jpg",
"hash": "0gfmeo.jpg",
"sha1": "7dc14ad3f0c273ce0188aeef19a6104e81ba67dd",
"uploaded": 1747557347,
"ip": "::1",
"useragent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/136.0.0.0 Safari\/537.36",
"delete_code": "z94fd5tyto1u2bww23kec0f52irb7x49",
"delete_url": "http:\/\/localhost:8080\/delete_z94fd5tyto1u2bww23kec0f52irb7x49\/0gfmeo.jpg",
"remote_port": "35856"
}
```
# Redis index
If redis caching is enabled, this structure will be created on the fly:
- cache:byurl:<url> => <content controller>;<file_name> # cached response
- served:<file_name> => number of views # view count

View File

@@ -7,4 +7,5 @@
- [ ] Add a way for users to report content
- [ ] Review reports
- [ ] Delete content if needed in a way that the user can't reupload it
- [ ] Add a way to delete content that is older than X days
- [ ] Add a way to delete content that is older than X days
- [ ] Bruteforce protection for admin login

View File

@@ -3,12 +3,11 @@ services:
pictshare:
image: 'HaschekSolutions/pictshare:3'
environment:
- SERVER_NAME=:80 # required by caddy
- URL=http://localhost:8080/
- MAX_UPLOAD_SIZE=20 #in MB
# Security settings
- ADMIN_PASSWORD= # password for the admin panel. if empty, admin panel is disabled
- ALLOWED_SUBNET= #IP address or subnet mask to allow upload to the server
- ALLOWED_SUBNET= #IP address or subnet mask to allow upload and access to the admin panel
- CONTENTCONTROLLERS= # limit uploaded file types
- MASTER_DELETE_CODE= # code to delete all files
- MASTER_DELETE_IP= # IP address to delete all files even without delete code
@@ -43,6 +42,7 @@ services:
- REDIS_CACHING=true
- REDIS_SERVER=/run/redis/redis.sock
- REDIS_PORT=0
- SERVER_NAME=:80 # required by caddy
ports:
- 8080:80
volumes:

View File

@@ -29,26 +29,54 @@ function architect($u)
{
$forbidden = true;
}
return renderTemplate('main.html.php',['forbidden'=>$forbidden]);
return renderTemplate('index.html.php',['main'=>renderTemplate('main.html.php',['forbidden'=>$forbidden])]);
}
// admin logic
if($u[0] == 'admin' && defined('ADMIN_PASSWORD') && ADMIN_PASSWORD != '')
{
// block checks
if (defined('ALLOWED_SUBNET') && ALLOWED_SUBNET != '' && !isIPInRange(getUserIP(), ALLOWED_SUBNET))
return;
else if (defined('MASTER_DELETE_IP') && MASTER_DELETE_IP != '' && !isIPInRange(getUserIP(), MASTER_DELETE_IP))
return;
session_start();
if($_REQUEST['password'] && $_REQUEST['password']== ADMIN_PASSWORD)
{
$_SESSION['admin'] = true;
switch($u[1]){
case 'stats':
if(!$_SESSION['admin'])
header('Location: /admin');
return renderTemplate('index.html.php',['main'=>renderTemplate('admin.stats.html.php',['stats'=>getStats()])]);
case 'logs':
if(!$_SESSION['admin'])
header('Location: /admin');
switch($u[2])
{
case 'app':
return renderTemplate('index.html.php',['main'=>renderTemplate('admin.logs-table.html.php',['type'=>'app','logs'=>getLogs('app',$u[3])])]);
case 'error':
return renderTemplate('index.html.php',['main'=>renderTemplate('admin.logs-table.html.php',['type'=>'error','logs'=>getLogs('error',$u[3])])]);
case 'views':
return renderTemplate('index.html.php',['main'=>renderTemplate('admin.logs-table.html.php',['type'=>'views','logs'=>getLogs('views',$u[3])])]);
default:
return renderTemplate('index.html.php',['main'=>renderTemplate('admin.logs.html.php')]);
}
default:
if($_REQUEST['password'] && $_REQUEST['password']== ADMIN_PASSWORD)
{
$_SESSION['admin'] = true;
}
if($_SESSION['admin'])
{
if(isset($_REQUEST['logout']))
{
unset($_SESSION['admin']);
session_destroy();
}
}
return renderTemplate('index.html.php',['main'=>renderTemplate('admin.html.php')]);
}
if($_SESSION['admin'])
{
if(isset($_REQUEST['logout']))
{
unset($_SESSION['admin']);
session_destroy();
}
}
return renderTemplate('admin.html.php');
}
//check cache
@@ -59,7 +87,7 @@ function architect($u)
{
list($cc, $hash) = explode(';', $cache_data);
if(defined('LOG_VIEWS') && LOG_VIEWS===true)
addToLog("Cache hit: ".getUserIP()." viewed $hash\t".$_SERVER['HTTP_USER_AGENT'], ROOT.DS.'logs/views.log');
addToLog(getUserIP()."\tviewed\t$hash\tFrom cache. Agent:\t".$_SERVER['HTTP_USER_AGENT']."\tref:\t".$_SERVER['HTTP_REFERER'], ROOT.DS.'logs/views.log');
$GLOBALS['redis']->incr("served:$hash");
return (new $cc())->handleHash($hash,$u);
}
@@ -73,7 +101,7 @@ function architect($u)
{
$hash = $el;
if(defined('LOG_VIEWS') && LOG_VIEWS===true)
addToLog(getUserIP()." viewed $hash\tIt was locally found\t".$_SERVER['HTTP_USER_AGENT'], ROOT.DS.'logs/views.log');
addToLog(getUserIP()."\tviewed\t$hash\tIt was locally found. Agent:\t".$_SERVER['HTTP_USER_AGENT']."\tref:\t".$_SERVER['HTTP_REFERER'], ROOT.DS.'logs/views.log');
break;
}
// if we don't have a hash yet but the element looks like it could be a hash
@@ -93,7 +121,7 @@ function architect($u)
storeFile(ROOT.DS.'tmp'.DS.$hash,$hash,true);
if(defined('LOG_VIEWS') && LOG_VIEWS===true)
addToLog(getUserIP()." viewed $hash\tIt was found in Storage Controller $contr\t".$_SERVER['HTTP_USER_AGENT'], ROOT.DS.'logs/views.log');
addToLog(getUserIP()."\tviewed\t$hash\tIt was found in Storage Controller $contr. Agent:\t".$_SERVER['HTTP_USER_AGENT']."\tref:\t".$_SERVER['HTTP_REFERER'], ROOT.DS.'logs/views.log');
break; // we break here because we already have the file. no need to check other storage controllers
}
@@ -110,7 +138,7 @@ function architect($u)
unlink(ROOT.DS.'tmp'.DS.$el.'.enc');
if(defined('LOG_VIEWS') && LOG_VIEWS===true)
addToLog(getUserIP()." viewed $hash\tIt was found encrypted in Storage Controller $contr\t".$_SERVER['HTTP_USER_AGENT'], ROOT.DS.'logs/views.log');
addToLog(getUserIP()."\tviewed\t$hash\tIt was found encrypted in Storage Controller $contr. Agent:\t".$_SERVER['HTTP_USER_AGENT']."\tref:\t".$_SERVER['HTTP_REFERER'], ROOT.DS.'logs/views.log');
break; // we break here because we already have the file. no need to check other storage controllers
}
@@ -125,7 +153,7 @@ function architect($u)
if((new $cc)::ctype=='dynamic' && in_array((new $cc)->getRegisteredExtensions()[0],$u) )
{
if(defined('LOG_VIEWS') && LOG_VIEWS===true)
addToLog(getUserIP()." requested ".implode("/",$u)."\tIt's a dynamic image handled by $cc\t".$_SERVER['HTTP_USER_AGENT'], ROOT.DS.'logs/views.log');
addToLog(getUserIP()." requested ".implode("/",$u)."\tIt's a dynamic image handled by $cc. Agent:\t".$_SERVER['HTTP_USER_AGENT']."\tref:\t".$_SERVER['HTTP_REFERER'], ROOT.DS.'logs/views.log');
$hash = true;
break;
}
@@ -833,7 +861,7 @@ function getDeleteCodeOfHash($hash)
function getMetadataOfHash($hash)
{
$metadata = array();
$metadata = [];
$metadatafile = getDataDir().DS.$hash.DS.'meta.json';
if(file_exists($metadatafile))
{
@@ -1107,4 +1135,40 @@ function addToLog($data,$logfile=ROOT.DS.'logs/app.log')
$fp = fopen($logfile,'a');
fwrite($fp,date("d.m.y H:i")."\t[".getURL()."] | ".$data."\n");
fclose($fp);
}
function getStats(){
$stats = array();
$stats['total_files'] = 0;
$stats['total_size'] = 0;
$stats['total_files'] = count(glob(getDataDir().DS.'*', GLOB_ONLYDIR));
foreach (glob(getDataDir().DS.'*') as $dir) {
if (is_dir($dir)) {
$stats['hashes'][basename($dir)] = [
'size' => filesize($dir.DS.basename($dir)),
'files' => count(glob($dir.DS.'*')),
'views' => $GLOBALS['redis']->get('served:'.basename($dir)),
'metadata' => getMetadataOfHash(basename($dir)),
];
}
}
return $stats;
}
function getLogs($type='app',$filter=false)
{
$logs = array();
$logfile = ROOT.DS.'logs/'.$type.'.log';
if($type && file_exists($logfile))
{
$handle = fopen($logfile, "r");
if ($handle) {
while (($line = fgets($handle)) !== false) {
if($filter && strpos($line,$filter)===false) continue;
$logs[] = $line;
}
fclose($handle);
}
}
return $logs;
}

View File

@@ -1,93 +1,25 @@
<!DOCTYPE html>
<!--[if IEMobile 7 ]> <html class="no-js iem7"> <![endif]-->
<!--[if (gt IEMobile 7)|!(IEMobile)]><!-->
<html class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PictShare - the smart CDN</title>
<!-- Bootstrap -->
<link href="/css/bootstrap.min.css" rel="stylesheet">
<!-- PictShare overwrites -->
<link href="/css/pictshare.css" rel="stylesheet">
<link href="/css/dropzone.css" rel="stylesheet">
<link href="/css/hljs-dracula.css" rel="stylesheet">
<!-- github-fork-ribbon-css
https://simonwhitaker.github.io/github-fork-ribbon-css/ -->
<link href="/css/gh-fork-ribbon.min.css" rel="stylesheet">
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<script>
var maxUploadFileSize = <?php echo (int)(ini_get('upload_max_filesize')); ?>
</script>
<meta name="description" content="Free image sharing, linking and tracking">
<meta name="keywords" content="image, share, hosting, free">
<meta name="robots" content="index, follow">
<meta name="copyright" content="Haschek Solutions">
<meta name="language" content="EN,DE">
<meta name="author" content="Haschek Solutions">
<meta name="distribution" content="global">
<meta name="rating" content="general">
</HEAD>
<BODY>
<div class="container" id="headcontainer">
<div class="row">
<div class="col-md-8">
<a href="/"><img src="/css/imgs/logo/horizontalv3.png" /></a>
</div>
<h2>Admin Panel</h2>
<?php if (!$_SESSION['admin']) { ?>
<form method="post" action="/admin">
<div class="input-group mb-3">
<input type="password" class="form-control" name="password" placeholder="Password" aria-label="Password" aria-describedby="btn-addn">
<button class="btn btn-outline-secondary" type="submit" id="btn-addn">Login</button>
</div>
</div>
</form>
<?php } ?>
<?php if ($_SESSION['admin']) { ?>
<div class="alert alert-success" role="alert">You are logged in as admin</div>
<form method="post" action="/admin">
<button type="submit" name="logout" class="btn btn-danger">Logout</button>
</form>
<div class="container">
<h2>Admin Panel</h2>
<?php if (!$_SESSION['admin']) { ?>
<form method="post" action="/admin">
<div class="input-group mb-3">
<input type="password" class="form-control" name="password" placeholder="Password" aria-label="Password" aria-describedby="btn-addn">
<button class="btn btn-outline-secondary" type="submit" id="btn-addn">Login</button>
</div>
</form>
<?php } ?>
<?php if ($_SESSION['admin']) { ?>
<div class="alert alert-success" role="alert">You are logged in as admin</div>
<form method="post" action="/admin">
<button type="submit" name="logout" class="btn btn-danger">Logout</button>
</form>
<ul class="nav">
<li class="nav-item">
<a class="nav-link" aria-current="page" href="/admin/stats">Stats</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/logs">Logs</a>
</li>
</ul>
<ul class="nav">
<li class="nav-item">
<a class="nav-link" aria-current="page" href="#">Stats</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Review</a>
</li>
</ul>
<?php } ?>
</div>
<div class="container">
<footer class="text-center">(c)<?php echo date("y"); ?> by<br /><a href="https://haschek.solutions" target="_blank"><img height="30" src="/css/imgs/hs_logo.png" /></a></footer>
</div>
<script src="/js/pictshare.js"></script>
</BODY>
</HTML>
<?php } ?>

View File

@@ -0,0 +1,15 @@
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/admin">Admin</a></li>
<li class="breadcrumb-item"><a href="/admin/logs">Logs</a></li>
<li class="breadcrumb-item active" aria-current="page"><?=$type?></li>
</ol>
</nav>
<h1><?= ucfirst($type) ?></h1>
<pre>
<code><?php foreach($logs as $log):
echo $log;
endforeach; ?></code>
</pre>

View File

@@ -0,0 +1,10 @@
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/admin">Admin</a></li>
<li class="breadcrumb-item active" aria-current="page">Logs</li>
</ol>
</nav>
<a class="btn btn-primary" href="/admin/logs/app">General App logs</a>
<a class="btn btn-primary" href="/admin/logs/views">Views</a>
<a class="btn btn-primary" href="/admin/logs/error">Errors</a>

View File

@@ -0,0 +1,41 @@
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/admin">Admin</a></li>
<li class="breadcrumb-item active" aria-current="page">Stats</li>
</ol>
</nav>
<h1>Stats</h1>
<div class="table-responsive">
<table class="table table-hover" data-toggle="table" data-search="true">
<thead>
<tr>
<th scope="col" data-sortable="true">Hash</th>
<th scope="col" data-sortable="true">Views</th>
<th scope="col" data-sortable="true">Original Filename</th>
<th scope="col" data-sortable="true">MIME type</th>
<th scope="col" data-sortable="true">Created at</th>
<th scope="col" data-sortable="true">Uploader IP</th>
<?php if(defined('LOG_VIEWS') && LOG_VIEWS==true): ?>
<th scope="col" data-sortable="true">List views</th>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php foreach($stats['hashes'] as $hash => $data): ?>
<tr>
<th scope="row"><a href="/<?=$hash?>"><?=$hash?></a></th>
<td><?=$data['views']?:0?></td>
<td><?=$data['metadata']['original_filename']?></td>
<td><?=$data['metadata']['mime']?></td>
<td><?=date("Y-m-d H:i",$data['metadata']['uploaded'])?></td>
<td><?=$data['metadata']['ip']?></td>
<?php if(defined('LOG_VIEWS') && LOG_VIEWS==true): ?>
<td scope="col" data-sortable="true"><a class="btn btn-secondary" href="/admin/logs/views/<?=$hash?>">View logs</a></td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>

View File

@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PictShare - the smart CDN</title>
<!-- Bootstrap -->
<link href="/css/bootstrap.min.css" rel="stylesheet">
<!-- PictShare overwrites -->
<link href="/css/pictshare.css" rel="stylesheet">
<link href="/css/dropzone.css" rel="stylesheet">
<link href="/css/bootstrap-table.min.css" rel="stylesheet">
<link href="/css/hljs-dracula.css" rel="stylesheet">
<!-- github-fork-ribbon-css
https://simonwhitaker.github.io/github-fork-ribbon-css/ -->
<link href="/css/gh-fork-ribbon.min.css" rel="stylesheet">
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<script>
var maxUploadFileSize = <?php echo (int)(ini_get('upload_max_filesize')); ?>
</script>
<meta name="description" content="Free image sharing, linking and tracking">
<meta name="keywords" content="image, share, hosting, free">
<meta name="robots" content="index, follow">
<meta name="copyright" content="Haschek Solutions">
<meta name="language" content="EN,DE">
<meta name="author" content="Haschek Solutions">
<meta name="distribution" content="global">
<meta name="rating" content="general">
</HEAD>
<BODY>
<div class="container" id="headcontainer">
<div class="row">
<div class="col-md-8">
<a href="/"><img src="/css/imgs/logo/horizontalv3.png" /></a>
</div>
</div>
</div>
<div id="main" class="container hv-100">
<?=$main;?>
</div>
<div class="footer">
<div class="container text-center">
<p>created by <a href="https://haschek.solutions" target="_blank"><img height="30" src="/css/imgs/hs_logo.png" /></a></p>
</div>
</div>
<script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-table.min.js"></script>
<script src="/js/dropzone.js"></script>
<script src="/js/highlight.pack.js"></script>
<script src="/js/pictshare.js"></script>
</BODY>
</HTML>

View File

@@ -1,161 +1,100 @@
<!DOCTYPE html>
<!--[if IEMobile 7 ]> <html class="no-js iem7"> <![endif]-->
<!--[if (gt IEMobile 7)|!(IEMobile)]><!-->
<html class="no-js"> <!--<![endif]-->
<?php
if (file_exists(ROOT . DS . 'notice.txt'))
echo '<div class="alert alert-warning" role="alert">' . file_get_contents(ROOT . DS . 'notice.txt') . '</div>';
?>
<div class="well">
<?php if ($forbidden === true) { ?>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PictShare - the smart CDN</title>
<h2>Upload forbidden</h2>
<!-- Bootstrap -->
<link href="/css/bootstrap.min.css" rel="stylesheet">
<p>Due to configured restrictions, you are not allowed to upload files at this time</p>
<!-- PictShare overwrites -->
<link href="/css/pictshare.css" rel="stylesheet">
<link href="/css/dropzone.css" rel="stylesheet">
<link href="/css/hljs-dracula.css" rel="stylesheet">
<!-- github-fork-ribbon-css
https://simonwhitaker.github.io/github-fork-ribbon-css/ -->
<link href="/css/gh-fork-ribbon.min.css" rel="stylesheet">
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<script>
var maxUploadFileSize = <?php echo (int)(ini_get('upload_max_filesize')); ?>
</script>
<meta name="description" content="Free image sharing, linking and tracking">
<meta name="keywords" content="image, share, hosting, free">
<meta name="robots" content="index, follow">
<meta name="copyright" content="Haschek Solutions">
<meta name="language" content="EN,DE">
<meta name="author" content="Haschek Solutions">
<meta name="distribution" content="global">
<meta name="rating" content="general">
</HEAD>
<BODY>
<a class="github-fork-ribbon left-top" href="https://github.com/HaschekSolutions/pictshare" data-ribbon="Fork me on GitHub" title="Fork me on GitHub">Fork me on GitHub</a>
<div class="container" id="headcontainer">
<div class="row">
<div class="col-md-8">
<a href="/"><img src="/css/imgs/logo/horizontalv3.png" /></a>
<?php
if (file_exists(ROOT . DS . 'notice.txt'))
echo '<div class="alert alert-warning" role="alert">' . file_get_contents(ROOT . DS . 'notice.txt') . '</div>';
?>
<div class="well">
<?php if ($forbidden === true) { ?>
<h2>Upload forbidden</h2>
<p>Due to configured restrictions, you are not allowed to upload files at this time</p>
<?php } else { ?>
<div id="uploadinfo"></div>
<p>
Max Upload size: <?=(int)(ini_get('upload_max_filesize'))?>MB / File<br/>
Allowed file types: <?= implode(', ', getAllContentFiletypes()) ?>
<?php
if (defined('UPLOAD_CODE') && UPLOAD_CODE != ''):?>
<br>Upload Code: <input type="password" id="uploadcode" />
<?php endif; ?>
</p>
<form class="dropzone well" id="dropzone" method="post" action="/api/upload" enctype="multipart/form-data">
<div class="fallback">
<input name="file" type="file" multiple />
</div>
</form>
<?php } ?>
</div>
<?php } else { ?>
<div id="uploadinfo"></div>
<p>
Max Upload size: <?= (int)(ini_get('upload_max_filesize')) ?>MB / File<br />
Allowed file types: <?= implode(', ', getAllContentFiletypes()) ?>
<?php
if (defined('UPLOAD_CODE') && UPLOAD_CODE != ''): ?>
<br>Upload Code: <input type="password" id="uploadcode" />
<?php endif; ?>
</p>
<form class="dropzone well" id="dropzone" method="post" action="/api/upload" enctype="multipart/form-data">
<div class="fallback">
<input name="file" type="file" multiple />
</div>
</div>
</form>
<?php } ?>
</div>
<h2 id="api" class="section-heading">Using PictShare</h2>
<div class="row">
<div class="col-6">
<h3>Basics</h3>
<p>
When you upload an image you'll get a link like this:
<pre><code class="url"><?= getURL() ?>abcef123.jpg</code></pre>
You can modify the size of the image by adding a size parameter to the URL. For example this will render the image in 800x600:
<pre><code class="url"><?= getURL() ?>800x600/abcef123.jpg</code></pre>
</p>
<p>
If you want to force the size to really be 800x600, you can use the "forcesize" parameter. It will still keep the aspect ratio but zoom in as needed to fit the dimensions
<pre><code class="url"><?= getURL() ?>800x600/forcesize/abcef123.jpg</code></pre>
</p>
<p>There are many more of these <a target="_blank" href="https://github.com/HaschekSolutions/pictshare/blob/master/rtfm/MODIFIERS.md">modifiers</a> and even <a target="_blank" href="https://github.com/HaschekSolutions/pictshare/blob/master/rtfm/IMAGEFILTERS.md">filters</a> available</p>
</div>
</div>
<hr class="no-print border border-primary border-2 opacity-50">
<h2 id="api" class="section-heading">Using the API</h2>
<div class="row">
<div class="col-6">
<h2>Basics</h2>
<p>
All API calls are done via GET or POST requests. The API will return JSON encoded data.
</p>
Base URL
<pre><code class="url"><?= getURL() ?>api</code></pre>
</div>
<div class="container">
<h2 id="api" class="section-heading">Using PictShare</h2>
<div class="row">
<div class="col-6">
<h3>Basics</h3>
<p>
When you upload an image you'll get a link like this:<pre><code class="url"><?= getURL() ?>abcef123.jpg</code></pre>
You can modify the size of the image by adding a size parameter to the URL. For example this will render the image in 800x600:
<pre><code class="url"><?= getURL() ?>800x600/abcef123.jpg</code></pre>
</p>
<p>
If you want to force the size to really be 800x600, you can use the "forcesize" parameter. It will still keep the aspect ratio but zoom in as needed to fit the dimensions
<pre><code class="url"><?= getURL() ?>800x600/forcesize/abcef123.jpg</code></pre>
</p>
<p>There are many more of these <a target="_blank" href="https://github.com/HaschekSolutions/pictshare/blob/master/rtfm/MODIFIERS.md">modifiers</a> and even <a target="_blank" href="https://github.com/HaschekSolutions/pictshare/blob/master/rtfm/IMAGEFILTERS.md">filters</a> available</p>
</div>
</div>
<hr class="no-print border border-primary border-2 opacity-50">
</div>
<div class="container">
<h2 id="api" class="section-heading">Using the API</h2>
<div class="row">
<div class="col-6">
<h2>Basics</h2>
<p>
All API calls are done via GET or POST requests. The API will return JSON encoded data.
</p>
Base URL
<pre><code class="url"><?= getURL() ?>api</code></pre>
</div>
<div class="col-6">
<h2>Error handling</h2>
When the status is err there will always be a field "reason" that explains what went wrong.
<pre><code class="json">
<div class="col-6">
<h2>Error handling</h2>
When the status is err there will always be a field "reason" that explains what went wrong.
<pre><code class="json">
{
"status": "err",
"reason": "File not a valid image"
}
</code></pre>
</div>
</div>
<div class="w-100">
<hr />
</div>
<div class="w-100">
<hr />
</div>
<div class="col-6">
<h2>Uploading an image</h2>
<div class="col-6">
<h2>Uploading an image</h2>
API call
<pre><code class="url">/upload</code></pre>
API call
<pre><code class="url">/upload</code></pre>
<p>You can post a file using the POST variable <span class="badge text-bg-secondary">file</span></p>
<p>You can post a file using the POST variable <span class="badge text-bg-secondary">file</span></p>
CURL example
<pre><code class="bash">curl -s -F "file=@myphoto.jpg" "<?= getURL() ?>upload"</code></pre>
CURL example
<pre><code class="bash">curl -s -F "file=@myphoto.jpg" "<?= getURL() ?>upload"</code></pre>
Output
<pre><code class="json">
Output
<pre><code class="json">
{
"status": "ok",
"hash": "7eli4d.jpg",
@@ -164,21 +103,21 @@
"delete_code": "jxgat3wze8lmn9sqwxy4x32p2xm7211g",
"delete_url": "http://localhost:8080/delete_jxgat3wze8lmn9sqwxy4x32p2xm7211g/7eli4d.jpg"
}</code></pre>
</div>
<div class="col-6">
<h2>Grabbing a URL</h2>
</div>
<div class="col-6">
<h2>Grabbing a URL</h2>
API call
<pre><code class="url">/upload?url=<span class="badge text-bg-secondary">url of image</span></code></pre>
API call
<pre><code class="url">/upload?url=<span class="badge text-bg-secondary">url of image</span></code></pre>
<p>You can specify a URL in the POST or GET variable <span class="badge text-bg-secondary">url</span>
that the call will try to download and process.</p>
<p>You can specify a URL in the POST or GET variable <span class="badge text-bg-secondary">url</span>
that the call will try to download and process.</p>
CURL example
<pre><code class="bash">curl -s "<?= getURL() ?>api/upload?url=https://pictshare.net/d2j1e1.png</code></pre>
CURL example
<pre><code class="bash">curl -s "<?= getURL() ?>api/upload?url=https://pictshare.net/d2j1e1.png</code></pre>
Output
<pre><code class="json">
Output
<pre><code class="json">
{
"status": "ok",
"hash": "ysj455.webp",
@@ -188,24 +127,24 @@
"delete_url": "http://localhost:8080/delete_4l0w04l4s42xddt2s5mrj1wikxz11l5z/ysj455.webp"
}
</code></pre>
</div>
</div>
<div class="w-100">
<hr />
</div>
<div class="w-100">
<hr />
</div>
<div class="col-6">
<h2>Uploading Base64 encoded content</h2>
<p>It's also possible to upload supported files via Base64 strings by providing the <span class="badge text-bg-secondary">base64</span> http parameter</p>
<div class="col-6">
<h2>Uploading Base64 encoded content</h2>
<p>It's also possible to upload supported files via Base64 strings by providing the <span class="badge text-bg-secondary">base64</span> http parameter</p>
API call
<pre><code class="url">/upload/?base64=<span class="badge text-bg-secondary">base64 encoded string</span></code></pre>
API call
<pre><code class="url">/upload/?base64=<span class="badge text-bg-secondary">base64 encoded string</span></code></pre>
CURL example
<pre><code class="bash">(echo -n "base64="; echo -n "data:image/jpeg;base64,$(base64 -w 0 Screenshot3.jpg)") | curl --data @- <?= getURL() ?>api/upload</code></pre>
CURL example
<pre><code class="bash">(echo -n "base64="; echo -n "data:image/jpeg;base64,$(base64 -w 0 Screenshot3.jpg)") | curl --data @- <?= getURL() ?>api/upload</code></pre>
Output
<pre><code class="json">
Output
<pre><code class="json">
{
"status": "ok",
"hash": "5e6alk.jpg",
@@ -215,23 +154,9 @@
"delete_url": "http://localhost:8080/delete_7ha2b5ccvsuvdd3qdnegzb2zqa9zxb5t/5e6alk.jpg"
}</code></pre>
</div>
<div class="col-6">
</div>
</div>
<div class="col-6">
</div>
</div>
<div class="container">
<footer>(c)<?php echo date("y"); ?> by<br /><a href="https://haschek.solutions" target="_blank"><img height="30" src="/css/imgs/hs_logo.png" /></a></footer>
</div>
<script src="/js/dropzone.js"></script>
<script src="/js/highlight.pack.js"></script>
<script src="/js/pictshare.js"></script>
</BODY>
</HTML>
</div>

10
web/css/bootstrap-table.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -6,6 +6,7 @@ body {
background-image: url('imgs/bg.png');
background-repeat: no-repeat;
background-color: #fff;
padding-bottom: 60px; /* Space for the footer */
}
#headcontainer
@@ -32,4 +33,13 @@ body {
.box__success,
.box__error {
display: none;
}
.footer {
position: fixed;
bottom: 0;
width: 100%;
height: 60px; /* Set the fixed height of the footer here */
line-height: 60px; /* Vertically center the text there */
background-color: #f5f5f5;
}

10
web/js/bootstrap-table.min.js vendored Normal file

File diff suppressed because one or more lines are too long

7
web/js/bootstrap.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
web/js/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -2,90 +2,92 @@ Dropzone.autoDiscover = false;
hljs.initHighlightingOnLoad();
document.addEventListener("DOMContentLoaded", function () {
var myDropzone = new Dropzone("#dropzone");
if (typeof maxUploadFileSize !== "undefined")
myDropzone.options.maxFilesize = maxUploadFileSize;
if (document.getElementById("dropzone") != null) {
var myDropzone = new Dropzone("#dropzone");
if (typeof maxUploadFileSize !== "undefined")
myDropzone.options.maxFilesize = maxUploadFileSize;
myDropzone.options.timeout = 0;
myDropzone.options.timeout = 0;
myDropzone.on("sending", function(file, xhr, formData) {
var uploadCodeElem = document.getElementById("uploadcode");
if (uploadCodeElem)
formData.append("uploadcode", uploadCodeElem.value);
});
myDropzone.on("sending", function (file, xhr, formData) {
var uploadCodeElem = document.getElementById("uploadcode");
if (uploadCodeElem)
formData.append("uploadcode", uploadCodeElem.value);
});
myDropzone.on('error', function(file, response) {
var uploadInfo = document.getElementById("uploadinfo");
if (response == null || response == "null") {
uploadInfo.insertAdjacentHTML("beforeend",
renderMessage(
"Error uploading " + file.name,
"Reason is unknown :(",
"danger"
)
);
} else {
if (response.status == 'err') {
myDropzone.on('error', function (file, response) {
var uploadInfo = document.getElementById("uploadinfo");
if (response == null || response == "null") {
uploadInfo.insertAdjacentHTML("beforeend",
renderMessage(
"Error uploading " + file.name,
"Reason: " + response.reason,
"Reason is unknown :(",
"danger"
)
);
} else {
if (response.status == 'err') {
uploadInfo.insertAdjacentHTML("beforeend",
renderMessage(
"Error uploading " + file.name,
"Reason: " + response.reason,
"danger"
)
);
} else {
uploadInfo.insertAdjacentHTML("beforeend",
renderMessage(
"Error uploading " + file.name,
"Reason: " + response,
"danger"
)
);
}
}
});
myDropzone.on("success", function (file, response) {
console.log("raw response: " + response);
var uploadInfo = document.getElementById("uploadinfo");
if (response == null || response == "null") {
uploadInfo.insertAdjacentHTML("beforeend",
renderMessage(
"Error uploading " + file.name,
"Reason: " + response,
"Reason is unknown :(",
"danger"
)
);
} else {
if (response.status == 'ok') {
uploadInfo.insertAdjacentHTML("beforeend",
renderMessage(file.name + " uploaded as <a target='_blank' href='/" + response.hash + "'>" + response.hash + "</a>", "URL: <a target='_blank' href='" + response.url + "'>" + response.url + "</a> <button class='btn btn-primary btn-sm' onClick='navigator.clipboard.writeText(\"" + response.url + "\");'>Copy URL</button>", "success")
);
} else if (response.status == 'err') {
uploadInfo.insertAdjacentHTML("beforeend",
renderMessage(
"Error uploading " + file.name,
response.reason,
"danger"
)
);
}
}
}
});
});
myDropzone.on("success", function (file, response) {
console.log("raw response: " + response);
var uploadInfo = document.getElementById("uploadinfo");
if (response == null || response == "null") {
uploadInfo.insertAdjacentHTML("beforeend",
renderMessage(
"Error uploading " + file.name,
"Reason is unknown :(",
"danger"
)
);
} else {
if (response.status == 'ok') {
uploadInfo.insertAdjacentHTML("beforeend",
renderMessage(file.name+" uploaded as <a target='_blank' href='/" + response.hash + "'>" + response.hash + "</a>","URL: <a target='_blank' href='" + response.url + "'>" + response.url + "</a> <button class='btn btn-primary btn-sm' onClick='navigator.clipboard.writeText(\"" + response.url + "\");'>Copy URL</button>","success")
);
} else if (response.status == 'err') {
uploadInfo.insertAdjacentHTML("beforeend",
renderMessage(
"Error uploading " + file.name,
response.reason,
"danger"
)
);
document.onpaste = function (event) {
var items = (event.clipboardData || event.originalEvent.clipboardData).items;
for (var index in items) {
var item = items[index];
if (item.kind === 'file') {
myDropzone.addFile(item.getAsFile());
}
}
}
});
document.onpaste = function (event) {
var items = (event.clipboardData || event.originalEvent.clipboardData).items;
for (var index in items) {
var item = items[index];
if (item.kind === 'file') {
myDropzone.addFile(item.getAsFile());
}
}
};
};
}
});
function renderMessage(title,message,type){
if(!type)
function renderMessage(title, message, type) {
if (!type)
type = "danger";
return `<div class='alert alert-${type}' role='alert'><strong>${title}</strong><br/>${message}</div>`;
}