понедельник, 11 ноября 2013 г.

Создания сайта по обмену файлов с нуля

При использовании домашнего сервера и низкоуровневого хостинга без подключения готовой CMS все задачи по управлению контентом ложатся на администратора без использования готовых инструментов. Но в некоторых случаях это абсолютно приемлемо. Например файловый хостинг, где пользователям нужно загрузить файл и получить ссылку, которую в следствии можно переслать получателю, и он в свою очередь сможет скачать файл в удобное время.
Или другой вариант - не всегда на форумах или в комментариях можно загружать картинки, видео или аудио. В каких случаях предусмотрена публикация ссылок на сторонние ресурсы, так вот - сейчас мы напишем как сделать этот сторонний ресурс.

В качестве домена мы будем использовать имя tempfile.pp.ua. Сайт полностью рабочий - и уже работает.

Первым делом нужно избрать площадку для размещения самого сайта. Это может быть виртуальный сервер на хостинге, или свой домашний сервер, который имеет доступ с Интернета.
Примем по умолчанию что там уже установлено
Apache2 - вебсервер
Php5 - язык программирования
Mysql - база данных

База данных
Создадим базу данных, где будут храниться все служебные данные
Выполним такие команды в консоли:
mysqladmin create tempfile_db
mysql_setpermission

где  mysqladmin create tempfile_db - создаст базу данных, а mysql_setpermission - создать пользователя с паролем, правами и доступом только с локального хоста

Создаем таблицы для хранения данных
Таблица завершенных загрузок
CREATE TABLE uploads(
id INT(11) NOT NULL auto_increment,
mail text,
activation_code text(32),
created timestamp,
last_activated timestamp,
filename text,
path text,
type text,
link text,
activated bool,
access int(11),
PRIMARY KEY (id))


Таблица временных загрузок
CREATE TABLE tmp_files(
id INT(11) NOT NULL auto_increment, 
created timestamp, 
sid text(32), 
name text,
tmpname text,
type text, 
PRIMARY KEY (id))

После этого в настройках вебсервера, создадим учетную запись виртуального сервера для сайта

<VirtualHost *:80>
    ServerName tempfile.pp.ua
    ServerAlias www.tempfile.pp.ua
    DocumentRoot /home/www/tempfile.pp.ua/
    ErrorLog /home/www/tempfile.pp.ua/logs/error.log

<Directory "/home/www/tempfile.pp.ua/">
    Options Indexes FollowSymLinks
    AllowOverride AuthConfig FileInfo
    Header add Access-Control-Allow-Origin "*"
    Order allow,deny
    Allow from all
    Require all granted
</Directory>
</VirtualHost>


Обязательно нужно включить модуль ReWrite
LoadModule rewrite_module /usr/lib/apache2/modules/mod_rewrite.so

Переходим к созданию файловой структуры каталогов
/home/www/tempfile.pp.ua
--->~ru
--->~en
--->images
--->include
--->logs
--->scripts
--->style
--->templates
--->~uploads

~ru,~en - являются символьными ссылками на каталог /home/www/tempfile.pp.ua. Это позволит перенаправлять пользователей с нужным переводом.
images,scripts, style -  это папки содержащие картинки с оформлением сайта, джаваскрипты и файлы стилей.
~uploads - символьная ссылка на объемное хранилище для файлов.
include - модули

Заполняем файл /home/www/tempfile.pp.ua/.htaccess
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
#RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L,QSA]
</IfModule>


Это позволит нам перенаправлять все запросы на нужный исполняемый файл, чем с самого начала обезопасит внедрения в запросы ссылок на системные файлы.

Далее создаем .htaccess содержащий строку
Deny from All

и копируем файл  в такие папки - uploads, logs,include

Предварительно разложим файлы которые нужны для работы
В папочку scripts
jquery-1.9.1.min.js
jquery.ui.widget.js
scripts/jquery.iframe-transport.js
scripts/jquery.fileupload.js

scripts/jquery.fileupload-fp.js

И стили
style/style.css

В корне сайта создаем такие файлы
index.php 
config.php

В настройках пропишем основные настройки
<?php
define (DB_NAME,"tempfile_db");
define (DB_USER,"tempfile");
define (DB_PASS,"megapassword");
define (UPLOAD_PATH,$_SERVER['DOCUMENT_ROOT'].'/uploads');

define (MAIL_TITLE,"Upload files informer");
define (WEBMASTER, "admin@tempfile.pp.ua");


Текст файла index.php

<?php
include $_SERVER['DOCUMENT_ROOT'].'/config.php';

$url_path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uri_parts = explode('/', $url_path);

// Основной код обработки
if ($uri_parts[1]=='ru') {$rus=1; array_shift($uri_parts);}
if ($uri_parts[1]=='en') {$rus=0; array_shift($uri_parts);}
include 'include/header.php';
try {
if ($uri_parts[1]) {
$filename=$_SERVER['DOCUMENT_ROOT'].'/include/'.$uri_parts[1].'.php';
if (file_exists($filename)) include $filename; else throw new Exception();
}
else.
if ($rus) include $_SERVER['DOCUMENT_ROOT'].'/include/home_rus.php';
else include $_SERVER['DOCUMENT_ROOT'].'/include/home.php';
}catch(Exception $e){
include $_SERVER['DOCUMENT_ROOT'].'/include/404.html';
}
include $_SERVER['DOCUMENT_ROOT'].'/include/footer.php';


// Окончание основного кода обработки 
?>




Основная задача этого скрипта - получить управление и вызвать нужные модули из каталога include. По умолчанию мы всегда показываем содержимое, которое генерится файлами header.php и footer.php как шаблон заголовка и подвала сайта, а между ними собственно само содержимое, чтобы исключить дублирование информации во всех файлах. Если не указан конкретный скрипт, то по умолчанию вызываем home.php.
Если указан каталог языка - то мы выбираем язык и далее все тоже самое. Обязательная проверка на наличие файлов и обработка ошибок. Это обезопасит от внедрения команд злоумышлениками.

По скольку табличная верстка уже давно считается плохим тоном, то и мы используем блочную. А в данном случае иначе не имеет смысла.
header.php
<?php
if ($rus) include $_SERVER['DOCUMENT_ROOT'].'/include/header_rus.php';
else
echo '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Upload and share files for free</title>
<meta name="robots" content="index,follow">
<meta name="description" content="Free filestorage for all. No popups, no waitings, no slow speed - only fast and free file sharing.">
<meta name="keywords" content="upload files, share files, share for free, download files">
<meta name="document-state" content="state">
<meta name="revisit-after" content="1 days">
<meta http-equiv="content-type" content="text/html; charset=utf8">
<link href="style/style.css" rel="stylesheet" type="text/css">
<script type="text/javascript" src="scripts/jquery.min.js"></script>
<body>
<div id=content><div id=languages>
<a href=/en>English</a> | <a href=/ru>Russian</a>
</div>
<div id=logo1><a href=/ title="Home"><img src=images/baby_top.png></a></div>
<H1><a href=/>Upload & share files for free</a></H1>';
?>


footer.php
<?php
if ($rus) { include $_SERVER['DOCUMENT_ROOT'].'/include/footer_rus.php';
exit;      }
?>
<div id=logo2><a href=/ title="Upload & share files for free"><img src=images/baby_bottom.png></a></div>
</div>
<div id="footer">
<a href="about">About</a> | <a href="terms">User agreement</a> | <a href="contacts">Contacts</a>..
</div>
<div id="copyrights"> Copyrights 2013</div>
</body>

</html>

Как видно из примера - все переведенные файлы подключаются из основного скрипта ответственного каждый за свою работу. Не нужно создавать файл с кучей переменных и переводить при генерации содержимого. Данный способ имеет недостаток - если добавлять функционал, то нужно добавлять его во все файлы с переводом. А поскольку у нас всего 2 языка - то это не очень большая проблема.

Основной модуль главной страницы практически статичен, исключая один момент - он генерирует переменную sid в скрытом элементе input.
home.php
<div id=form>
<table id="1form">
<tr><td nobr>Select files: <td>
<form action="upload" method="post" enctype="multipart/form-data">
<input id="fileupload" type="file" name="files[]" multiple="" >
</form>
<tr><td>Email:<td>.
<form action="upload" method="post">
<input type="hidden" name="sid" id="sid" value='<?php echo md5(time()); ?>'>
<input name="email" id="email" placeholder="Enter email for notification">
</table>
<div><input type=checkbox id=read_lic> I have read <a href="terms">user agreement.</a><br />
<span id=info>Max files size per 1 upload - 1Gb. Maximum files per 1 upload - 100</span><br />
<br>
<center>
<input type="submit" value="Place files" id="button_submit" onClick="return checkMail();">
</center>
</div>
<div id="progress" class="progress">
        <div class="progress-bar progress-bar-success">%</div>
    </div>
<div id="console"></div>
</form>
</div>
<script src="scripts/jquery.ui.widget.js"></script>
<script src="scripts/jquery.iframe-transport.js"></script>
<script src="scripts/jquery.fileupload.js"></script>
<script src="scripts/jquery.fileupload-fp.js"></script>
<style>
progress-bar {
    width: 0px;
    height: 100%;
    font-size: 12px;
    color: rgb(255, 255, 255);
    text-align: center;
    background-color: rgb(66, 139, 202);
    box-shadow: 0px -1px 0px rgba(0, 0, 0, 0.15) inset;
    transition: width 0.6s ease 0s;
}
.progress-bar-success {
    background-color: rgb(92, 184, 92);
}
</style>
<br>
<script type="text/javascript">
function checkMail(){
       var mailformat = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
  if (document.getElementById('read_lic').checked!=1)


   alert("You have not read user agreement!");
   return false;
}
if ($('#email').val().length===0) {
    if (confirm("With email your files will be available for 365 days, \n Without email notification your files will be available for 10 days only!\n Continue without email?"))
           return true;

    else 
          return false;
}
       else
       if($('#email').val().match(mailformat))..
       {

           return true;
       }else {
   alert("You have entered incorrect email!");
   return false;
}
}
$(function () {
    'use strict';
    var d=new Date();
    var url = "include/uploadfile.php?sid="+$('#sid').val()+"&"+d.getTime();
    $('#fileupload').fileupload({
        url: url,
        dataType: 'script',
           progress: function (e, data) {
            var progress = parseInt(data.loaded / data.total * 100, 10);
            $('#progress .progress-bar').css( 'width', progress + '%' );
   $('#progress .progress-bar ').html(progress + '%');
}
    }).prop('disabled', !$.support.fileInput)
        .parent().addClass($.support.fileInput ? undefined : 'disabled');
});
</script>


В тексте содержатся два дополнительных скрипта - проверка введен ли е-мейл пользователя и загрузка файлов на сервер во временное хранилище. Скрипт загрузки взят из академического примера по jquery - fileupload.
Для чего используется две формы? Очень просто - пока пользователь выбирает файлы - они уже загружаются на сервер. А после подтверждения, ему не прийдется ждать. Если использовать одну форму - то после 1-й загрузки, файлы снова будут грузиться на сервер, а это может быть проблемой если файлы будут больших объемов.

Для работы с базой данных нам понадобится модуль работы с ней db.php:
<?php
class Tdb{
var $db;
var $result;
var $num_rows;
var $affected;
function Tdb($dbhost='localhost',$dbname=DB_NAME,$dbuser=DB_USER,$dbpass=DB_PASS){
$this->db=@mysql_connect($dbhost, $dbuser,$dbpass) or die('Could not connect: ' . mysql_error());
@mysql_select_db($dbname, $this->db) or die('Could not select database.');
}
function query($sql){
$this->result = @mysql_query($sql);
$this->num_rows = @mysql_num_rows($this->result);
$this->affected = @mysql_affected_rows($this->result);
}
function get_row(){
if ($this->num_rows)
return @mysql_fetch_row($this->result);
}
}
?>


Это практически копия с руководства по программированию баз данных mysql на php.net.


Скрипт загрузки временных файлов uploadfile.php
<?php
include $_SERVER['DOCUMENT_ROOT'].'/config.php';
 

$df=disk_free_space(UPLOAD_PATH.'/');
// проверяем наличие бо 1Гб свободного места, на всякий случай для служебных целей
if ($df<1073741824) {
echo "$('#console').html($('#console').html()+\"Space limit exceeded<br />\");";
}
try {
if ($_FILES){
//если загружены файлы копируем их во временный каталог ../uploads/tmp
if (is_uploaded_file($_FILES['files']['tmp_name'][0])){
include $_SERVER['DOCUMENT_ROOT'].'/include/db.php';
$sid=$_GET['sid'];
$name=$_FILES['files']['name'][0];
$tmpname=$_FILES['files']['tmp_name'][0];
$type=$_FILES['files']['type'][0];
@mkdir(UPLOAD_PATH.'/tmp/'.$sid);
@copy($tmpname,UPLOAD_PATH.'/tmp/'.$sid.'/'.$name);
$sz=filesize(UPLOAD_PATH.'/tmp/'.$sid.'/'.$name);
if ($sz<1024) $size="$sz bytes";
else if ($sz>1024 && $sz<1048576) $size=round(intval($sz)/1024,2) . " Kbytes";
else if ($sz>1048576) $size=round(intval($sz)/1048576,2) . " Mbytes";
$db=new Tdb();
 

//сохраняем данные о сесии
$db->query(sprintf("INSERT INTO tmp_files(sid,created,name,tmpname,type) VALUES('%s',NOW(),'%s','%s','%s')",

mysql_real_escape_string($sid),
mysql_real_escape_string($name),
mysql_real_escape_string($tmpname),
mysql_real_escape_string($type)
);
//сообщаем об успешности операции
echo "$('#console').html($('#console').html()+\"$name ($size)<br />\");";
}
}else throw new Exception();
}catch (Exception $e){

// если что-то пошло не так - сообщаем
echo "$('#console').html($('#console').html()+\"files did not upload<br />\");";
}
?>


Ключевым моментом есть использование правильной формы для построения mysql-запросов. Данный способ является безопасным с точки зрения SQL-инжектов, поэтому все данные полученные от пользователя должны проходить такую проверку ибо цена - потеря данных, а то и полный доступ к вашему серверу злоумышленников.

Скрипт подтверждения upload.php
<?php
if ($rus){ include $_SERVER['DOCUMENT_ROOT'].'/include/upload_rus.php';}
else {
function logger($s){
$logfile=$_SERVER['DOCUMENT_ROOT']."/logs/log.txt";
if (file_exists($logfile)) $f=fopen($logfile,"a+");
else $f=fopen($logfile,"w+");
fwrite($f,date("Y-m-d h:i").' '.$s."\n");
fclose($f);
}

include $_SERVER['DOCUMENT_ROOT'].'/include/db.php';
$db=new Tdb();
if ($_POST){
 

//проверяем есть ли устаревшые файлы которые нужно удалить
include $_SERVER['DOCUMENT_ROOT'].'/include/checktimeout.php'; 


$sid=$_POST['sid'];
$email=$_POST['email'];
 

//формируем список временных файлов от данного пользователя
$db->query(sprintf("SELECT name,type from tmp_files where sid='%s'",mysql_real_escape_string($sid)));
$rows=$db->num_rows;
for($i=0;$i<$rows;$i++) $row[$i]=$db->get_row();



//Переносим из временной папки в постоянную с созданием прямой ссылки
for($i=0;$i<$rows;$i++){
list($name,$type)=$row[$i];
$link=md5("$sid $name");
$path="http://".$_SERVER["SERVER_NAME"];
@rename(UPLOAD_PATH.'/tmp/'.$sid.'/'.$name,UPLOAD_PATH.'/'.$link);
 $sql=sprintf("INSERT INTO uploads(mail,activation_code,created,filename,path,type,link,access,activated) VALUES('%s','%s',NOW(),'%s','%s','%s','%s',0,0)",
mysql_real_escape_string($email),
mysql_real_escape_string($sid),
mysql_real_escape_string($name),
mysql_real_escape_string($path),
mysql_real_escape_string($type),
mysql_real_escape_string($link)
);
$db->query($sql);
 

//заполняем таблицу  со списком файлов
$output.="<tr><td>".($i+1)."<td align=left>$type<td align=left><a href=\"$path/get?id=$link\">$name</a><td><a href=\"$path/get?id=$link\" id=\"link$i\">$path/get?id=$link</a>";
}
@rmdir(UPLOAD_PATH.'/tmp/'.$sid);//удаляем временный каталог
}

$output="<style>
#filelist {
border-spacing: 1px;
background-color: gray;
font-size: 10pt;
}
#filelist td{
padding: 5px;
background-color: white;
text-align:left;
}
#filelist th{
padding: 5px;
background-color: #c0c0c0;
}
</style>
<table id=filelist>
<caption>Uploaded files:</caption>
<tr><th>#<th>Type<th>Name<th>Link".$output."</table>
";

if ($rows){

// если список не пуст - показываем пользователю список
echo "". $output."
<p>Your files will be available for <b>10 days</b>. <br />";


// если был введен емейл - показываем уведомление
if ($email) {
echo "Letter with activation code was sent to your email <b>$email</b>. <br />
If you activate your files from email, they will be available for <b>365 days</b>. </p>";


//Получаем заготовку письма
$html=file_get_contents($_SERVER['DOCUMENT_ROOT'].'/templates/mail_upload.html');

// Меняем служебные слова [date], [activation_link],[output] - на соответствующие данные
$html=preg_replace('/\[date\]/',date('l jS \of F Y h:i:s A'),$html);
$html=preg_replace('/\[activation_link\]/','http://'.$_SERVER['SERVER_NAME'].'/activate?id='.$sid,$html);
$html=preg_replace('/\[output\]/',$output,$html);


// отправляем по почте уведомление
mail($email,"upload & share files for free",$html,
'MIME-Version: 1.0' . "\r\n" ..
'Content-type: text/html; charset=utf8' . "\r\n"..
'To: '.$email . "\r\n"..
'From: '.MAIL_TITLE.' <'.WEBMASTER.'>' . "\r\n");
}
}else{

// если ничего не загружено или список пуст
echo "<p>No files were uploaded</p>";
}


// предлагаем продолжить
echo "<input type=button id=\"button_addNew\" value=\"Add new\" onClick=\"window.location.href='/';\">";
}
?>


Скрипт проверки годности файлов checktimeout.php
<?php
// выбираем и удаляем временные файлы, которые не были удалены, или подтверджены пользователями
$db->query("SELECT sid,name FROM tmp_files WHERE created<DATE_SUB(NOW(), INTERVAL 1 DAY)");
for($i=0;$i<$db->num_rows;$i++){
list($sid,$name)=$db->get_row();
@unlink(UPLOAD_PATH."/tmp/$sid/$name");
logger("delete after 1 days: ".UPLOAD_PATH."/tmp/$sid");
@rmdir(UPLOAD_PATH."/tmp/$sid");
}
$db->query("DELETE FROM tmp_files WHERE created<DATE_SUB(NOW(), INTERVAL 1 DAY)");

// выбираем и удаляем файлы, которые не были активированы
$db->query("SELECT link FROM uploads WHERE activated!=true AND created<DATE_SUB(NOW(), INTERVAL 10 DAY)");
for($i=0;$i<$db->num_rows;$i++){
list($link)=$db->get_row();
logger("delete after 10 days: ".UPLOAD_PATH."/$link");
@unlink(UPLOAD_PATH."/$link");
}
$db->query("DELETE FROM uploads WHERE activated!=true AND created<DATE_SUB(NOW(), INTERVAL 10 DAY)");



// выбираем и удаляем временные файлы, которые были активированы больше года назад
$db->query("SELECT link FROM uploads WHERE activated=true AND last_activated<DATE_SUB(NOW(), INTERVAL 365 DAY)");
for($i=0;$i<$db->num_rows;$i++){
list($link)=$db->get_row();
logger("delete after 365 days: ".UPLOAD_PATH."/$link");
@unlink(UPLOAD_PATH."/$link");
}
$db->query("DELETE FROM uploads WHERE activated=true AND last_activated<DATE_SUB(NOW(), INTERVAL 365 DAY)");
?>


После того как пользователь загрузил файлы, он может подтвердить их через почту перейдя по ссылке activate.php

<?php
if (isset($_GET['id'])){
include $_SERVER['DOCUMENT_ROOT'].'/config.php';
include $_SERVER['DOCUMENT_ROOT'].'/include/db.php';
$db=new Tdb();
$db->query(sprintf("UPDATE uploads set activated=true,last_activated=NOW() where activation_code='%s'",mysql_real_escape_string($_GET['id'])));
echo "<p>Your files will be places for 365 days from now. You are welcome.</p>
<input type=button id=\"button_addNew\" value=\"Add new\" onClick=\"window.location.href='/';\">";
}
?>


Остался последний штрих - получить файлы по ссылке. В данном случае мы расположим код в файле index.php

<?php
include $_SERVER['DOCUMENT_ROOT'].'/config.php';
$url_path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uri_parts = explode('/', $url_path);

if ($uri_parts[1]=='get') {
$id=$_GET['id'];
include $_SERVER['DOCUMENT_ROOT'].'/include/db.php';
$db=new Tdb();

$db->query(sprintf("SELECT type,filename from uploads WHERE link='%s'",

mysql_real_escape_string( $id)));
list($type,$name)=$db->get_row();
$size=filesize(UPLOAD_PATH.'/'.$id);
// увеличим счетчик обращений к файлу
$db->query(sprintf("UPDATE uploads SET access=access+1 WHERE link='%s'",

mysql_real_escape_string( $id)));

header("Content-type: $type");
header("Content-Length: $size");

// Если файл картинка - показываем его в браузере, если другое - скачиваем
if (preg_match('/image/',$type))
  header("Content-Disposition: filename=$name");
else
   header("Content-Disposition: attachment; filename=$name");

// непосредственно считываем и отдаем пользователю файл
readfile(UPLOAD_PATH.'/'.$id);
}else.
{
// предыдущее содержимое скрипта

}
?>

Прописав основные стили в файле style/style.css - можно начинать пользоваться сервисом
body {background-color: yellow;}
#content
{
position:relative;
width: 800px;
height: 480px auto;
background-color: white;
border: 1px solid black;
border-radius:10px;
padding: 10px;
margin: auto;
text-align: center;
}
#footer{
padding: 3px;
text-align: center;
font-size: 8pt;
}
#copyrights{
padding: 3px;
text-align: center;
font-size: 8pt;
color: gray;
}
#form
{
width: 400px;
border: 1px solid #c0c0c0;
margin: auto;
text-align:left;
padding: 15px;
border-radius: 10px;
}
#progressbar {width: 100%;}
#button_addNew {
margin: 10px;
padding: 15px;
border: 1px solid #c0c0c0;
border-radius: 10px;
background-color: rgb(66, 139, 202);
color: white;
font-size: 14pt;
}
#button_submit {
margin: 10px;
padding: 15px;
border: 1px solid #c0c0c0;
border-radius: 10px;
background-color: rgb(66, 139, 202);
color: white;
font-size: 14pt;
}
#fileupload {
padding: 5px;
border: 1px solid #c0c0c0;
border-radius: 10px;
background-color: rgb(66, 139, 202);
color: white;
font-size: 11pt;
}
#logo1{
top:0px;
height: 165px;
}
#logo2{
position: absolute;
height: 98px;
top: 173px;
width:800px;
text-align:center;
}
#info {
font-size: 8pt;
}
#console {
font-size: 8pt;
}
#email {
width:100%;
}
#languages{
position:absolute;;
right: 10px;
top:3px;
font-size:8pt;
}
#1form {
width:100%;
}
a img{
border:0;
}


Но для начала нужно временно прописать настройки DNS локально в файле
Для Windows - 
C:\WINDOWS\SYSTEM32\Drivers\etc\hosts.etc
Для Линукс
/etc/hosts

77.120.87.4    tempfile.pp.ua

где 77.120.87.4 - айпи адрес сервера.

Но можно пойти и другим путем, зарегистрировав официально себе домен.
В данном случае был использован сервис nic.ua.

Комментариев нет:

Отправить комментарий