Servlet. Загрузка файла на сервер (Upload)

 
 
 
Сообщения:862
В примере будут испльзоватся Servlet 2.5, для версии 2.4 тоже должно работать.

Часть 1. Подготовка формы
Для того, что бы отправить файл на сервер с помщью браузера. Первым делом нужно правильно сформировать форму для отправки запроса. Выглядеть она должна примерно так:
<html>
<head>
<title>Upload</title>
</head>
<body>
	<form action="http://localhost:8080/upload" method="post" enctype="multipart/form-data">
		<input name="description" type="text"><br>
		<input name="data" type="file"><br>
		<input type="submit"><br>
	</form>
</body>
</html>

Теперь подробно рассмотрим все аттрибуты тега form, все они являются обязательными, без них работать не будет.

action="http://localhost:8080/upload"; - указывает адрес, куда будет направлен запрос, вместо http://localhost:8080/upload можно указать любой другой URL. Можно использовать относительные URLы.

method="post" - указывает кокого типа запрос будет сформирован.
По умолчанию аттрибут имеет значение get. Особенностью метода POST является, то что POST запрос может включать в себя произвольные данные. Эти данные обычно называют телом запроса(телом POST запроса). Файлы передаются как-раз в теле запроса.

enctype="multipart/form-data" - указывает браузеру каким образом должно формироваться тело запроса. По умолчанию имеет значение application/x-www-form-urlencoded. Когда в теле запроса передаются данные, они передаются не в сыром виде, так как они хранятся на диске или в памяти. Они предварительно кодируются. Атрибут enctype говорит каким способом нужно кодировать. Браузер добавляет к запросу заголовок Content-Type:enctype-value, где enctype-value это то, что указано в атрибуте enctype (в нашем случае multipart/form-data). Плюс этот заголовок может хранить ещё какие-то дополнительные данные. Сервер этот заголовок прочитает, и на основе его будет знать как раскодировать тело запроса. Или поймёт, что он не знает как это сделать. Именно такая ситуация в сервлетах до версии 2.5 включительно, в версии 3.0 ситуация изменилась. Сервер умеет раскодировать только application/x-www-form-urlencoded закодированные данные. Он понятия не имеет что делать multipart/form-data закодированными данными. И по этому приходится добавлять сторонние библиотеки которые берут эту задачу на себя.

Часть 2. Что делает сервер.
Можно пропустить и читать сразу следующую часть, здесь в основном теория для понимания процесса в целом.

Напишем, сервлет который будет этот запрос принимать.

package sevlet;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class Upload extends HttpServlet {
	protected void doPost(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {		
		
		String description = request.getParameter("description");
		String data = request.getParameter("data");
		
		response.getWriter().println("description="+description);
		response.getWriter().println("data="+data);
	}	
}
Сервлет должен прочитать параметры, и отправить их обратно в виде текста.
Развернём сервлет и отправим на него запрос. В поле description вписываем, что угодно, в поле data выбираем любой файл. В результате получим:
Quote:
description=null
data=null

То есть ничего хорошего не получим. Параметр description и тот не получим даже. Давайте пошагово разберём, что там произошло.
Для отправки запроса, я использовал браузер Mozila Firefox, предварительно установив плагин LiveHTTPheaders, который показывает запросы и ответы в том виде в котором они передаются браузером.

Шаг 1.
В поле description ввёл qwerty. В качестве data указал файл data.txt в котором был стих Пушкина парус. И нажал отправить запрос.
Вид запроса который я получил с помощью плагина:
Quote:
http://localhost:8080/upload

POST /upload HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 (.NET CLR 3.5.30729)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ru,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: windows-1251,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Content-Type: multipart/form-data; boundary=---------------------------74482520013458
Content-Length: 650
-----------------------------74482520013458
Content-Disposition: form-data; name="description"

qwerty
-----------------------------74482520013458
Content-Disposition: form-data; name="data"; filename="data.txt"
Content-Type: text/plain

I"a`?o'n~
A'a*e"a*a*o` i"a`?o'n~ i^a"e`i'i^e^e`e'
A^ o`o'i`a`i'a* i`i^?y" a~i^e"o'a'i^i`!...
?o`i^ e`u`a*o` i^i' a^ n~o`?a`i'a* a"a`e"a*e^i^e'?
?o`i^ e^e`i'o'e" i^i' a^ e^?a`? ?i^a"i'i^i`?...

E`a~?a`?o` a^i^e"i'u^ - a^a*o`a*? n~a^e`u`a*o`,
E` i`a`?o`a` a~i'a*o`n~y" e` n~e^?u^i"e`o`...
O'a^u^, - i^i' n~?a`n~o`e`y" i'a* e`u`a*o`
E` i'a* i^o` n~?a`n~o`e`y" a'a*?e`o`!

I"i^a" i'e`i` n~o`?o'y" n~a^a*o`e"a*e' e"a`c,o'?e`,
I'a`a" i'e`i` e"o'? n~i^e"i'o"a` c,i^e"i^o`i^e'...
A` i^i', i`y"o`a*?i'u^e', i"?i^n~e`o` a'o'?e`,
E^a`e^ a'o'a"o`i^ a^ a'o'?y"o~ a*n~o`u" i"i^e^i^e'!
-----------------------------74482520013458--

Все вполне читаемо человеческим глазом, вместо паруса мы видим хрень потому, что это оно так русский текст закодировало.

Если не указать аттрибут enctype, или указать enctype=application/x-www-form-urlencoded, то этот же запрос будет выглядеть так. В этом случае браузер содержимое файла не трогает, а передает только имя фала. И видно, что тело запроса закодировано по другому(попроще).
Quote:

http://localhost:8080/upload

POST /upload HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 (.NET CLR 3.5.30729)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ru,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: windows-1251,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 32
description=qwerty&data=data.txt


Шаг 2.
Браузер устанавливает соединение с сервером и начинает передавать
этот запрос. Сервер начинает читать его. Дочитывает все до строчки Content-Length:650 включительно. Потом смотрит заголовок Content-Type: multipart/form-data; boundary=---------------------------74482520013458. Видит там multipart/form-data и понимает, что не умеет такое орабатывать. Дальше он даже не дочитывает запрос до конца.

Шаг 3.
Довайте модернизируем наш сервлет, и дочитаем остаток запроса вручную.
package sevlet;

import java.io.IOException;
import java.io.InputStreamReader;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class Upload extends HttpServlet {
	protected void doPost(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {		
		
		String description = request.getParameter("description");
		String data = request.getParameter("data");
		
		response.getWriter().println("description="+description);
		response.getWriter().println("data="+data);
		
		response.getWriter().println("POST request body:");
		
		InputStreamReader reader = new InputStreamReader(request.getInputStream());
		int c;
		while ((c=reader.read())>=0) {
			response.getWriter().print((char)c);
		}
	}	
}

Этот сервлет отправит в ответ, тело запроса(ту часть, что не дочитал сервер) в том виде что получил, без расшифровки. Отправим аналогичный запрос на обновленный сервлет. В результате получим:
Quote:
description=null
data=null
POST request body:
-----------------------------204722362218538
Content-Disposition: form-data; name="description"

qwerty
-----------------------------204722362218538
Content-Disposition: form-data; name="data"; filename="data.txt"
Content-Type: text/plain

?????
?????? ????? ????????
? ?????? ???? ???????!...
??? ???? ?? ? ?????? ????????
??? ????? ?? ? ???? ???????...

?????? ????? - ????? ??????,
? ????? ?????? ? ???????...
???, - ?? ??????? ?? ????
? ?? ?? ??????? ?????!

??? ??? ????? ??????? ??????,
??? ??? ??? ?????? ???????...
? ??, ????????, ?????? ????,
??? ????? ? ????? ???? ?????!
-----------------------------204722362218538--


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

Часть 3. Upload файла с помощью библиотеки FileUpload от Apache.
Первым делом нужно эту библиотеку скачать, скачать можно тут http://commons.apache.org/fileupload/, также она зависит от библиотеки IO ее тоже качаем http://commons.apache.org/io/. Закидываем их в WEB-INF/lib.
Теперь напишем сервлет, который будет принимать multipart/form-data запрос. И сохранять на сервере все файлы которые будут отправлены. Обычные параметры будет просто выводить в консоль.
package sevlet;

import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

public class Upload extends HttpServlet {	
	private Random random = new Random();
	
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		//проверяем является ли полученный запрос multipart/form-data
		boolean isMultipart = ServletFileUpload.isMultipartContent(request);
		if (!isMultipart) {
			response.sendError(HttpServletResponse.SC_BAD_REQUEST);
			return;
		}

		// Создаём класс фабрику 
		DiskFileItemFactory factory = new DiskFileItemFactory();

		// Максимальный буфера данных в байтах,
		// при его привышении данные начнут записываться на диск во временную директорию
		// устанавливаем один мегабайт
		factory.setSizeThreshold(1024*1024);
		
		// устанавливаем временную директорию
		File tempDir = (File)getServletContext().getAttribute("javax.servlet.context.tempdir");
		factory.setRepository(tempDir);

		//Создаём сам загрузчик
		ServletFileUpload upload = new ServletFileUpload(factory);
		
		//максимальный размер данных который разрешено загружать в байтах
		//по умолчанию -1, без ограничений. Устанавливаем 10 мегабайт. 
		upload.setSizeMax(1024 * 1024 * 10);

		try {
			List items = upload.parseRequest(request);
			Iterator iter = items.iterator();
			
			while (iter.hasNext()) {
			    FileItem item = (FileItem) iter.next();

			    if (item.isFormField()) {
			    	//если принимаемая часть данных является полем формы			    	
			        processFormField(item);
			    } else {
			    	//в противном случае рассматриваем как файл
			        processUploadedFile(item);
			    }
			}			
		} catch (Exception e) {
			e.printStackTrace();
			response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
			return;
		}		
	}
	
	/**
	 * Сохраняет файл на сервере, в папке upload.
	 * Сама папка должна быть уже создана. 
	 * 
	 * @param item
	 * @throws Exception
	 */
	private void processUploadedFile(FileItem item) throws Exception {
		File uploadetFile = null;
		//выбираем файлу имя пока не найдём свободное
		do{
			String path = getServletContext().getRealPath("/upload/"+random.nextInt() + item.getName());					
			uploadetFile = new File(path);		
		}while(uploadetFile.exists());
		
		//создаём файл
		uploadetFile.createNewFile();
		//записываем в него данные
		item.write(uploadetFile);
	}

	/**
	 * Выводит на консоль имя параметра и значение
	 * @param item
	 */
	private void processFormField(FileItem item) {
		System.out.println(item.getFieldName()+"="+item.getString());		
	}
}

Для того, чтобы пример работал, необходимо вручную создать папку upload в корне web - приложения.
Теперь, отправляем например выше упомянутый запрос, смотрим результат.

В добавок скажу, что сохранять файлы можно в любом месте, не обязательно в директории web приложения. Главное, что бы у процесса в котором запущен сервер были права на запись.

Пример полного приложения можно скачать тут.
 
 
Сообщения:82
Офигеть. Столько это искал...

А у меня будет свой форум с флудилкой и чатом...
 
 
Сообщения:3874
Спасибо, очень круто!
 
 
Сообщения:1259
А в чём разница с over9000 аналогичных примеров в интернете? Кроме того, что Пушкин к "Парусу" никакого отношения не имеет. Да, и у самого апача, помнится неплохие HowTo были.
 
 
Сообщения:862
Это пример для версии Servlets < 3.0. В версии 3.0 и выше можно использовать @MultipartConfig и request.getParts(). Пример:
http://www.codejava.net/java-ee/servlet/java-file-upload-example-with-servlet-30-api
Изменен:05 окт 2015 10:22
 
 
Сообщения:46
Что-то не получается

package servlet;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import utils.UtilsReadSrteam;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

@WebServlet("/upload")
public class ServletUpload extends HttpServlet implements UtilsReadSrteam {

    static int MAX_MEMORY_BUFER_SIZE = 1024 * 1024; // 1 Мегабайт
    static int MAX_SIZE_REQUEST = 1024 * 1024 * 10; // 10 Мегабайт

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String description = request.getParameter("description");
        String data = request.getParameter("data");

        request.setCharacterEncoding("UTF-8");

        response.setContentType("text/html");
        response.setCharacterEncoding("UTF-8");

        PrintWriter out = response.getWriter(); //отрывает поток на клиента, для ответа

        /*
        проверяем является ли полученный запрос - multipart/form-data
         SC_BAD_REQUEST = 400; - это константа
         Если полученный запрос не является типом multipart/form-data,
         инвертируем false в true и помещаем в объект response, ошибку
         с кодом 400
         */
        boolean isMultipart = ServletFileUpload.isMultipartContent(request);
        if (!isMultipart) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }

        /*создадим репозиторий для того, чтобы обеспечить временное хранение для файлов,
        * то есть создадим временную директорию
        * servletContext - получим сведения об среде веб-приложения
        * */

        ServletContext servletContext = this.getServletConfig().getServletContext();
        File repository = (File) servletContext.getAttribute("javax.servlet.context.tempdir");
        System.out.println(repository);

       /* Создаем класс фабрику для файлов находящихся на диске
         * данный объект позволит аккумулировать в себе другие объекты
         * Установим Максимальный размер буфера данных в байтах - устанавливаем один мегабайт,
       При его привышении данные начнут записываться на диск
       во временную директорию
       То есть установим ограничения
       */

        DiskFileItemFactory factory = new DiskFileItemFactory(MAX_MEMORY_BUFER_SIZE, repository);

        /*Создаём сам загрузчик*/
        ServletFileUpload upload = new ServletFileUpload(factory);

        /*Задаем общее ограничение на размер запроса
        * максимальный размер данных который разрешено загружать в байтах,
        * установим в 10 мбайт
        *  По умолчанию данное свойство устанволено в -1, что
        *  означает - без ограничений
        * */
        upload.setSizeMax(MAX_SIZE_REQUEST);

        /* анализируем запрос
        * */
        try {
            List<FileItem> items = upload.parseRequest(request);
            Iterator<FileItem> iter = items.iterator();
            while (iter.hasNext()) {
                FileItem item = iter.next();

                if (item.isFormField()) {
                    /*если принимаемая часть данных является полем формы,
                    тогда обрабатываем отдельно */
                    processFormField(item);
                } else {
                    /*если принимаемая часть данных не является
                    * полем формы, тогда рассматриваем ее как файл*/
                    processUploadedFile(item,request);
                }
            }
        } catch (FileUploadException e) {
            e.printStackTrace();
            /* если произойдет ошибка загрузки файлов, тогда
            *  клиенту отправляется ошибкас кодом 500*/
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            return;
        }


    }

    /* Сохраняет файл на сервере, в папке upload.
     * Сама папка должна быть уже создана.
     */
    private void processUploadedFile(FileItem item,HttpServletRequest request) {
        File uploadedFile = null;
        /*получаем случайное имя файла и получим абсолютный путь к загружаемому файлу
        * во временный каталог веб-приложения*/
        do {

            Random random = new Random();
            String fileName = item.getName();
            ServletContext context = request.getServletContext();
            String pathToFile = "/web/upload/" + random.nextInt(10) + fileName;
            String path = context.getRealPath(pathToFile);
            uploadedFile = new File(path);
        } while (uploadedFile.exists());

        //создаём файл
        try {
            uploadedFile.createNewFile();
            //записываем в него данные
            item.write(uploadedFile);
        } catch (IOException e) {
            e.printStackTrace();
        }catch (Exception e) {
            e.printStackTrace();
        }



    }

    /* Выводит на консоль имя параметра и значение
     */
    private void processFormField(FileItem item) {
        System.out.println(item.getFieldName() + " = " + item.getString());
    }
}




Вот здесь ловлю ошибку
//создаём файл
        try {
            uploadedFile.createNewFile();



java.io.IOException: Системе не удается найти указанный путь
	at java.io.WinNTFileSystem.createFileExclusively(Native Method)
	at java.io.File.createNewFile(File.java:1012)
	at servlet.ServletUpload.processUploadedFile(ServletUpload.java:133)
	at servlet.ServletUpload.doPost(ServletUpload.java:100)


Почему ?
 
 
Сообщения:861
А кто будет создавать файл? На духа всятого не уповайте :) Перед createNewFile() должен быть new File(...)
 
 
Сообщения:46
gidravlic:
Перед createNewFile() должен быть new File(...)


я учусь и поэтому я не знаю как это делается, нахожу примеры, пытаюсь их решить, но объяснений работы кода или нет или совсем мало...
 
 
Сообщения:46
new File(...) - это что ?
 
 
Сообщения:861
Пересмотрел код. new File(path) есть. 12 строка.
private void processUploadedFile(FileItem item,HttpServletRequest request) {
        File uploadedFile = null;
        /*получаем случайное имя файла и получим абсолютный путь к загружаемому файлу
        * во временный каталог веб-приложения*/
        do {
 
            Random random = new Random();
            String fileName = item.getName();
            ServletContext context = request.getServletContext();
            String pathToFile = "/web/upload/" + random.nextInt(10) + fileName;
            String path = context.getRealPath(pathToFile);
            uploadedFile = new File(path);
        } while (uploadedFile.exists());
 
        //создаём файл
        try {
            uploadedFile.createNewFile();
            //записываем в него данные
            item.write(uploadedFile);
        } catch (IOException e) {
            e.printStackTrace();
        }catch (Exception e) {
            e.printStackTrace();
        }
   }

Судя по ошибке, отсутствует путь, где должен создаваться этот файл.
 
 
Сообщения:46
Random random = new Random();
            String fileName = item.getName();
            ServletContext context = request.getServletContext();
            String pathToFile = "/web/upload/" + random.nextInt(10) + fileName;
            String path = context.getRealPath(pathToFile);
            uploadedFile = new File(path);


Так вот же этот код... И каталог этот есть в корне проекта, он создан вручную
Изменен:13 июн 2018 07:03
 
 
Сообщения:861
Ошибка говорит об обратном.
Перед uploadedFile.createNewFile() сделайте uploadedFile.mkdirs() чтобы убедиться, что путь создан.
 
 
Сообщения:46
Это не помогает .

Файл пытаются создать здесь
D:\javaEE_lessons\idea-project\upload-files\out\artifacts\upload_files_war_exploded\web\upload\210.jpg


Но загрузился он здесь

  ServletContext servletContext = this.getServletConfig().getServletContext();
        File repository = (File) servletContext.getAttribute("javax.servlet.context.tempdir");
        System.out.println(repository);



repository
профиль пользователя\.IntelliJIdea2017.2\system\tomcat\Tomcat_9_0_8_upload-files_2\work\Catalina\localhost\ROOT


tempFile
профиль пользователя\.IntelliJIdea2017.2\system\tomcat\Tomcat_9_0_8_upload-files_2\work\Catalina\localhost\ROOT\upload_00ceed55_409b_4732_bc5e_71ac9e515c9b_00000001.tmp
 
 
Сообщения:46
java.io.FileNotFoundException: D:\javaEE_lessons\idea-project\upload-files\out\artifacts\upload_files_war_exploded\web\upload\7Озеро.jpg (Отказано в доступе)
 
 
Сообщения:46
http://www.codejava.net/java-ee/servlet/apache-commons-fileupload-example-with-servlet-and-jsp

Если коротко - файлы необработанные при загрузке, помещаются во временный системный каталог Tomcat.
Затем вы получаете их путь, затем создаете еще один временный каталог, но уже в корне веб-приложения.
Затем получаете полный абсолютный путь к новому местоположению файла.
А после "сырой" файл из временного системного каталога, копируете под оригинальным именем в каталог в корне веб-приложения.

Данные обычных полей то же можете получить данные

try {
            List<FileItem> formItems = upload.parseRequest(request);

            if (formItems != null && formItems.size() > 0) {
                /*обрабатываем поля формы*/

                for (FileItem item : formItems) {
                    /*обработка тех полей, которые содержат файл*/
                    if (!item.isFormField()) {

						/*itemField - создаем объект, поле которого
                        будет хранить имя файла
						* nameField - возращает имя файла или каталога
						* filePath - получаем полный абсолютный путь до загружаемого файла,
						* во временный каталог на сервере
						* isVoidFileName - проверяем загружен ли файл
						* field - получаем название поля загружаемого файла*/
						String field = item.getFieldName();
                        File itemField = new File(item.getName());
                        String fileName = itemField.getName();
                        String filePath = uploadPath + File.separator + fileName;

                        FileUpload.isVoidFileName(fileName,field, filePath, session);


                        /*создаем объект для хранения полного пути файла*/
                        File storeFile = new File(filePath);


                        /*записываем файл на диск во временный каталог*/
                        item.write(storeFile);
                        request.setAttribute("message", "Загрузка успешна!");
                    } else if (item.isFormField()) {


                        /*если обрабатываемая часть данных - это поле формы
                        * тогда получаем значение данного поля и имя этого поля формы
                         * при получении значения обязательно указываем кодировку*/
                        String one = new String(item.get(), "UTF-8");
                        String nameField = item.getFieldName();
                        session.setAttribute(nameField, one);

                    }
                }
            }

        } catch (FileUploadException e) {
            e.printStackTrace();
        } catch (Exception e) {
            //e.printStackTrace();
            request.setAttribute("message", "При обработке запроса, произошла" +
                    "ошибка: " + e.getMessage());
        }



только не смог сделать очистку каталога upload.

не работает метод
uploadDir.deleteOnExit();
и ему подобные,
также не работает

<listener>
<listener-class>
org.apache.commons.fileupload.servlet.FileCleanerCleanup
</listener-class>
</listener>


при этом такой код работает только на
commons-fileupload-1.2.2.jar
commons-io-2.2.jar

на более новых библиотеках не заработал
Изменен:15 июн 2018 18:48
 
Модераторы:Нет
Сейчас эту тему просматривают:Нет