html текст
All interests
  • All interests
  • Design
  • Food
  • Gadgets
  • Humor
  • News
  • Photo
  • Travel
  • Video
Click to see the next recommended page
Like it
Don't like
Add to Favorites

ASP.NET MVC Урок 9. Configuration и загрузка файлов tutorial

Цель урока. Научиться использовать файл конфигурации Web.config. Application section, создание своих ConfigSection и IConfig. Научиться загружать файлы, использование file-uploader для загрузки файла, последующая обработка файла.

В этом уроке мы рассмотрим работу с конфигурационным файлом Web.config. Это xml-файл и в нем хранятся настройки программы.

Рассмотрим подробнее, из чего состоит этот файл:
  • configSection. Это секция отвечает за то, какие классы будут обрабатывать далее объявленные секции. Состоит из атрибута name — это тег, далее объявленной секции, и type – к какому классу относится.
  • connectionStrings. Это секция отвечает за работу с указанием строк инициализаций соединений с базами данных.
  • appSettings. Секция параметров типа key/value.
  • system.web, system.webServer. Секции параметров для работы веб-приложения.
  • runtime. Секция по настройке в режиме выполнения. Определение зависимостей между dll.
  • Остальные секции. Другие секции с параметрами, объявленными в configSection.




IConfig (и реализация).

Аналогично Repository, конфигуратор будем создавать как сервис. Создаем IConfig и Config-реализацию в папке Global (/Global/Config/IConfig.cs):
public interface IConfig
    {
        string Lang { get; }
    }

И
public class Config : IConfig
    {
        public string Lang
        {
            get 
            {
                return "ru";
            }
        }
    }

Добавляем строку в RegisterServices (/App_Start/NinjectWebCommon.cs):
 kernel.Bind<IConfig>().To<Config>().InSingletonScope();

И выводим в BaseController:
[Inject]
public IConfig Config { get; set; }


Теперь сделаем в инициализации контроллера переопеределение CultureInfo в потоке (/Controllers/BaseController.cs):
protected override void Initialize(System.Web.Routing.RequestContext requestContext)
        {
            try
            {
                var cultureInfo = new CultureInfo(Config.Lang);

                Thread.CurrentThread.CurrentCulture = cultureInfo;
                Thread.CurrentThread.CurrentUICulture = cultureInfo;
            }
            catch (Exception ex)
            {
                logger.Error("Culture not found", ex);
            }

            base.Initialize(requestContext);
        }


И добавим вывод даты в Index.cshtml (/Areas/Default/Views/Home/Index.cshtml):
    @DateTime.Now.ToString("D")


Получаем вывод:


И по-настоящему свяжем это с Web.Config. Добавим в Web.config в appSettings строку:
<add key="Culture" value="ru" />


В Config.cs (/Global/Config/Config.cs):
public string Lang
        {
            get 
            {
return ConfigurationManager.AppSettings["Culture"] as string;         
     }
        }

Запускаем – результат тот же, теперь изменим значение в Web.config на fr:
<add key="Culture" value="fr" />

Получаем дату:
mardi 5 mars 2013


Отлично! Можете попробовать еще с несколькими языками. Список сокращений находится тут http://msdn.microsoft.com/en-us/goglobal/bb896001.aspx

Создание своих типов ConfigSection

В этой части мы рассмотрим создание своих собственных ConfigSection. В этой главе мы реализуем загрузку файлов и создание превью. Нам понадобятся следующие данные: во-первых, зависимость mime-type от расширения, и иконка файлов (для скачивания, например):
  • расширение
  • mime-type
  • большая иконка
  • маленькая иконка


и во-вторых, данные для создания превью:
  • наименование превью (например, UserAvatarSize)
  • ширина
  • высота


Оба типа делаются одинаково, так что я распишу только создание одного из них. Пусть это будет IconSize, для создания превью. Первое, что надо сделать — это создать класс, наследуемый ConfigurationElement (/Global/Config/IconSize.cs):
public class IconSize : ConfigurationElement
    {
        [ConfigurationProperty("name", IsRequired = true, IsKey = true)]
        public string Name
        {
            get
            {
                return this["name"] as string;
            }
        }

        [ConfigurationProperty("width", IsRequired = false, DefaultValue = "48")]
        public int Width
        {
            get
            {
                return (int)this["width"];
            }
        }

        [ConfigurationProperty("height", IsRequired = false, DefaultValue = "48")]
        public int Height
        {
            get
            {
                return (int)this["height"];
            }
        }
    }


Рассмотрим подробнее:
  • ConfigurationProperty состоит из имени, это имя атрибута в строке
  • IsRequired – обязательный этот параметр или нет
  • IsKey – является ли ключом (как первичный ключ в БД)
  • DefaultValue – значение по умолчанию


Следующий шаг – это создание класса коллекции (так как у нас будет множество элементов) и секции (/Global/Config/IconSize.cs):
 public class IconSizesConfigSection : ConfigurationSection
    {
        [ConfigurationProperty("iconSizes")]
        public IconSizesCollection IconSizes
        {
            get
            {
                return this["iconSizes"] as IconSizesCollection;
            }
        }
    }

    public class IconSizesCollection : ConfigurationElementCollection
    {
        protected override ConfigurationElement CreateNewElement()
        {
            return new IconSize();
        }

        protected override object GetElementKey(ConfigurationElement element)
        {
            return ((IconSize)element).Name;
        }
}

В Web.config добавляем:
<iconConfig>
    <iconSizes>
      <add name="Avatar173Size" width="173" height="176" />
…
</iconSizes>
</iconConfig>



Теперь необходимо объявить класс разбора этой секции в configSection:
      <section name="iconConfig" type="LessonProject.Global.Config.IconSizesConfigSection, LessonProject" />


Обратите внимание, что в описание type необходимо указать имя dll (LessonProject), в которой он содержится. Это важно, но будет рассмотрено в unit-тестах.

MailSettings

Создадим одиночный конфиг для настроек по работе с smtp-почтой. Нам понадобятся:
  • SmtpServer. Имя сервера.
  • SmtpPort. Порт, обычно 25й.
  • SmtpUserName. Логин.
  • SmtpPassword. Пароль.
  • SmtpReply. Обратный адрес в строке Reply-to.
  • SmtpUser. Имя пользователя в строке From.
  • EnableSsl. Да/нет, использовать ли работу по Ssl.


Файл (/Global/Config/MailSetting.cs):
public class MailSetting : ConfigurationSection
    {
        [ConfigurationProperty("SmtpServer", IsRequired = true)]
        public string SmtpServer
        {
            get
            {
                return this["SmtpServer"] as string;
            }
            set
            {
                this["SmtpServer"] = value;
            }
        }

        [ConfigurationProperty("SmtpPort", IsRequired = false, DefaultValue="25")]
        public int SmtpPort
        {
            get
            {
                return (int)this["SmtpPort"];
            }
            set
            {
                this["SmtpPort"] = value;
            }
        }

        [ConfigurationProperty("SmtpUserName", IsRequired = true)]
        public string SmtpUserName
        {
            get
            {
                return this["SmtpUserName"] as string;
            }
            set
            {
                this["SmtpUserName"] = value;
            }
        }

        [ConfigurationProperty("SmtpPassword", IsRequired = true)]
        public string SmtpPassword
        {
            get
            {
                return this["SmtpPassword"] as string;
            }
            set
            {
                this["SmtpPassword"] = value;
            }
        }

        [ConfigurationProperty("SmtpReply", IsRequired = true)]
        public string SmtpReply
        {
            get
            {
                return this["SmtpReply"] as string;
            }
            set
            {
                this["SmtpReply"] = value;
            }
        }

        [ConfigurationProperty("SmtpUser", IsRequired = true)]
        public string SmtpUser
        {
            get
            {
                return this["SmtpUser"] as string;
            }
            set
            {
                this["SmtpUser"] = value;
            }
        }

        [ConfigurationProperty("EnableSsl", IsRequired = false, DefaultValue="false")]
        public bool EnableSsl
        {
            get
            {
                return (bool)this["EnableSsl"];
            }
            set
            {
                this["EnableSsl"] = value;
            }
        }
    }


Добавим в Web.config:
    <section name="mailConfig" type="LessonProject.Global.Config.MailSetting, LessonProject" />

И
  <mailConfig 
    SmtpServer="smtp.gmail.com" 
    SmtpPort="587" 
    SmtpUserName="lxndrpetrov" 
    SmtpPassword="**********" 
    SmtpReply="lxndrpetrov@gmail.com" 
    SmtpUser="test"
    EnableSsl="true" />


Добавим все это теперь в IConfig.cs и Сonfig.cs (/Global/Config/IConfig.cs):
public interface IConfig
    {
        string Lang { get; }

        IQueryable<IconSize> IconSizes { get; }

        IQueryable<MimeType> MimeTypes { get; }

        MailSetting MailSetting { get; }
    }


И
public IQueryable<IconSize> IconSizes
        {
            get 
            {
                IconSizesConfigSection configInfo = (IconSizesConfigSection)ConfigurationManager.GetSection("iconConfig");
                return configInfo.IconSizes.OfType<IconSize>().AsQueryable<IconSize>(); 
                
            }
        }

        public IQueryable<MimeType> MimeTypes
        {
            get
            {
                MimeTypesConfigSection configInfo = (MimeTypesConfigSection)ConfigurationManager.GetSection("mimeConfig");
                return configInfo.MimeTypes.OfType<MimeType>().AsQueryable<MimeType>();
            }
        }

        public MailSetting MailSetting
        {
            get 
            { 
                return (MailSetting)ConfigurationManager.GetSection("mailConfig");
            }
        }


Мы еще добавим MailTemplates — шаблоны которые нам понадобятся для рассылки email при регистрации, или при напоминании пароля.

Простая загрузка файлов


Сейчас рассмотрим стандартный пример загрузки файла на сервер, и больше никогда не будем пользоваться таким способом. Класс SimpleFileView для взаимодействия (/Models/Info/SimpleFileView.cs):
public class SimpleFileView
    {
        public HttpPostedFileBase UploadedFile { get; set; }
    }

Обратите внимание на наименование класса для приема файлов. Итак, создадим контроллер SimpleFileController (/Areas/Default/Controllers/SimpleFileController.cs):
public class SimpleFileController : DefaultController
    {
        [HttpGet]
        public ActionResult Index()
        {
            return View(new SimpleFileView());
        }

        [HttpPost]
        public ActionResult Index(SimpleFileView simpleFileView)
        {
            return View(simpleFileView);
        }
    }


И добавим View:
@model LessonProject.Models.Info.SimpleFileView
@{
    ViewBag.Title = "Index";
    Layout = "~/Areas/Default/Views/Shared/_Layout.cshtml";
}

<h2>Index</h2>

@using (Html.BeginForm("Index", "SimpleFile", FormMethod.Post, new {enctype = "multipart/form-data", @class = "form-horizontal" }))
{
    <fieldset>
        <div class="control-group">
            <label class="control-label" for="Email">
                Загрузите файл:</label>
            <div class="controls">
                @Html.TextBox("UploadedFile", Model.UploadedFile, new { type = "file", @class = "input-xlarge" })
                @Html.ValidationMessage("UploadedFile")
            </div>
        </div>
        <div class="form-actions">
            <button type="submit" class="btn btn-primary">
                Upload</button>
        </div>
    </fieldset>
}


Обратите внимание, на enctype в атрибутах формы и на type в атрибутах TextBox (на самом деле тип еще бывает password, checkbox, radio, но для них есть соответствующие методы в @Html-классе). Enctype необходимо установить в “multipart/form-data”, чтоб была возможность загрузить большой объём информации.

Загружаем и проверяем. Наш файл благополучно загружен, только необходимо сохранить InputStream в некий файл. Но оставим пока так и рассмотрим недостатки.

Первый недостаток – это то, что во всех браузерах форма выбора файла выглядит по-разному:



Конечно, ведь дизайнер представляет себе, что загрузка файлов выполняется как в Safari, а заказчик проверяет в Chrome и IE, и начинает спрашивать у разработчиков: «Что за самодеятельность?»
Второй недостаток –если форма не прошла валидацию, то эти поля необходимо выбрать заново. Т.е. есть такая форма:
  • Имя
  • Фамилия
  • Электронная почта
  • Дата рождения
  • Фотография
  • Фотография первого разворота паспорта
  • Фотография второго разворота паспорта
  • Фотография паспорта с пропиской
  • Пароль
  • Пароль еще раз
  • Капча


И вдруг вы набрали пароль неверно, или капчу не так ввели, или фотография второго разворота паспорта слишком большая, или вы забыли перегнать из raw-формата в jpeg.

В итоге фотографии, прописку и капчу надо вводить заново. Естественно, это совсем не user friendly, и раздражает заказчика (к тому же дизайнер нарисовал красиво, а выглядит убого).

Загрузка файла (ов) с помощью Ajax

Определим как должна вести себя загрузка файла:
  • Пользователь кликает на «загрузить».
  • Открывается форма выбора файла
  • Пользователь выбирает файл
  • Файл загружается, или выдается ошибка о том, что что-то не так
  • Если даже форма и не проходит валидацию, то файл остается загруженным и его не нужно загружать заново.


Это называется ajax-загрузка и для нее используем fineuploader (http://fineuploader.com/). Библиотека платная, но мы скачаем и соберем исходники (у нас же есть bundle!). Скачиваем исходники по ссылке: https://github.com/valums/file-uploader. Перемещаем js-файлы в папку /Scripts/fine-uploader. Css-файлы перемещаем в /Content и изображения в /Content/images. Перепишем правильно url в fineuploader.css для изображений:
.qq-upload-spinner {
    display: inline-block;
    background: url("images/loading.gif");
    width: 15px;
    height: 15px;
    vertical-align: text-bottom;
}
.qq-drop-processing {
    display: none;
}
.qq-drop-processing-spinner {
    display: inline-block;
    background: url("images/processing.gif");
    width: 24px;
    height: 24px;
    vertical-align: text-bottom;
}


Файлы инициализируем в BundleConfig.cs (/App_Start/BundleConfig.cs):

bundles.Add(new ScriptBundle("~/bundles/fineuploader")
                    .Include("~/Scripts/fine-uploader/header.js")
                    .Include("~/Scripts/fine-uploader/util.js")
                    .Include("~/Scripts/fine-uploader/button.js")
                    .Include("~/Scripts/fine-uploader/ajax.requester.js")
                    .Include("~/Scripts/fine-uploader/deletefile.ajax.requester.js")
                    .Include("~/Scripts/fine-uploader/handler.base.js")
                    .Include("~/Scripts/fine-uploader/window.receive.message.js")
                    .Include("~/Scripts/fine-uploader/handler.form.js")
                    .Include("~/Scripts/fine-uploader/handler.xhr.js")
                    .Include("~/Scripts/fine-uploader/uploader.basic.js")
                    .Include("~/Scripts/fine-uploader/dnd.js")
                    .Include("~/Scripts/fine-uploader/uploader.js")
                    .Include("~/Scripts/fine-uploader/jquery-plugin.js")
                    );
bundles.Add(new StyleBundle("~/Content/css/fineuploader")
                 .Include("~/Content/fineuploader.css"));


Создаем контроллер FileController.cs (/Areas/Default/Controllers/FileController.cs):
public class FileController : DefaultController
    {
        [HttpGet]
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult Upload(HttpPostedFileWrapper qqfile)
        {
            return Json(new { result = "ok", success = true});
        }
    }

Метод-action Upload принимает строковое значение qqfile, я ниже рассмотрю, почему так. А сейчас создадим View для Index. Для этого:
  • Создаем кнопку, при нажатии на которую мы загружаем файл.
  • Файл загружается и создается превью
  • Файл и превью сохраняются в файловую систему
  • Метод возвращает ссылку, куда были загружены файл и превью, через Json-ответ
  • Если файлы не удалось загрузить, то выдается соответствующая ошибка
  • Обрабатываем json-результат и уведомляем, что файл и превью загружено
  • Верификация формы и запись в БД не нужны.


View для Index:
@{
    ViewBag.Title = "Index";
    Layout = "~/Areas/Default/Views/Shared/_Layout.cshtml";
}

@section styles {
    @Styles.Render("~/Content/css/fineuploader")
}

@section scripts {
    @Scripts.Render("~/bundles/fineuploader")
    @Scripts.Render("~/Scripts/default/file-index.js")
}

<h2>Index</h2>

<fieldset>
    <div class="control-group">
        <label class="control-label" for="Text">
            Image
        </label>
        <div class="controls">
            <div id="UploadImage">
                Upload 
            </div>
        </div>
    </div>
    <div>
        <img src="" alt="" id="ImagePreview" />
    </div>
</fieldset>


Наша кнопка с id=UploadImage. Добавляем file-index.js файл для обработки (/Scripts/default/file-index.js):
function FileIndex() {
    _this = this;

    this.ajaxFileUpload = "/File/Upload";

    this.init = function () {
        $('#UploadImage').fineUploader({
            request: {
                endpoint: _this.ajaxFileUpload
            },
        }).on('error', function (event, id, name, reason) {
            //do something
        })
      .on('complete', function (event, id, name, responseJSON) {
          alert(responseJSON);
      });
    }
}

var fileIndex = null;

$().ready(function () {
    fileIndex = new FileIndex();
    fileIndex.init();
});



Теперь обработаем загрузку:
public ActionResult Upload(HttpPostedFileWrapper qqfile)
        {
            var extension = Path.GetExtension(qqfile.FileName);
            if (!string.IsNullOrWhiteSpace(extension))
            {
                var mimeType = Config.MimeTypes.FirstOrDefault(p => string.Compare(p.Extension, extension, 0) == 0);

                //если изображение
                if (mimeType.Name.Contains("image"))
                {
                    //тут сохраняем в файл
                    var filePath = Path.Combine("/Content/files", qqfile.FileName);
                    
                    qqfile.SaveAs(Server.MapPath(filePath));    
                    return Json(new
                    {
                        success = true,
                        result = "error",
                        data = new
                        {
                            filePath
                        }
                    });
                }
            }
            return Json(new { error = "Нужно загрузить изображение", success = false });
        } 


В Content добавим папку files — это будет папка пользовательских данных. Разберем код:
  • Получаем qqfile (тут ничего не поменять, это параметр обусловлен fineuploader).
  • Из него получаем extension.
  • По extension находим mimeType. Для .jpg, .gif, .png – мы получаем mime-type типа «image/…». Таким образом, мы проверяем, что этот файл можно загрузить.
  • Далее, используя имя файла, составляем абсолютный путь к папке /Content/files (которую мы заранее создали) с помощью Server.MapPath.
  • Далее сохраняем файл с помощью SaveAs.
  • Возвращаем имя файл в json data.filePath.


Проверяем, всё ли загружается, и приступим к созданию превью.

Создание превью

Во-первых, мы немного схитрили с mime-type = «image\...», ведь к ним относится и bmp, и tiff файлы, которые не поддерживаются браузерами.
Так что создадим класс PreviewCreator в проекте LessonProject.Tools (PreviewCreator.cs):
   public static class PreviewCreator
    {
public static bool SupportMimeType(string mimeType)
        {
            switch (mimeType)
            {
                case "image/jpg":
                case "image/jpeg":
                case "image/png":
                case "image/gif":
                    return true;
            }
            return false;
        }
    }


И заменим в FileController.cs (/Areas/Default/Controller/FileController.cs):
if (mimeType != null && PreviewCreator.SupportMimeType(mimeType.Name))


В PreviewCreator есть много функций для создания превью, так что я перечислю разные варианты создания изображения и подробно разберу один из них. Стоит учесть, что все превью создаются в формате jpeg. Итак, какие есть варианты:
  • Цветной и чернобелый вариант. Контролируется параметром grayscale (по умолчанию = false)
  • Превью. (CreateAndSavePreview) Если исходное изображение меньше, чем размеры превью, то изображение размещается посередине белого холста. Если по отношению к размерам исходный размер имеет вертикальную ориентированность (квадратик из портретного формата) – вырезаем верхнюю часть. Если же отношение горизонтально ориентированно относительно размера, то вырезаем середину.
  • Аватар. (CreateAndSaveAvatar) Если исходное изображение меньше, чем размеры превью, то изображение просто сохраняется. Если по отношению к размерам исходный размер имеет вертикальную ориентированность (квадратик из портретного формата) – то уменьшаем, по высоте. Если же отношение горизонтально ориентированно относительно размера, то вырезаем середину.
  • Изображение. (CreateAndSaveImage) Если изображение меньше, чем максимальные размеры, то сохраняем исходное. Если же изображение не вписывается в границы, то уменьшаем, чтобы оно не превышало максимальный размер, и сохраняем.
  • По размеру. (CreateAndSaveFitToSize) Если изображение меньше, чем размеры, то оно будет растянуто до необходимых размеров. С потерей качества, конечно же.
  • Обрезать. (CropAndSaveImage) Кроме стандартных параметров передаются координаты для обрезки изображения.


Cоздадим превью (CreateAndSavePreview), взяв из конфигурации размеры для создания превью AvatarSize (/Areas/Default/Controllers/FileController.cs):
var filePreviewPath = Path.Combine("/Content/files/previews", qqfile.FileName);
                    var previewIconSize = Config.IconSizes.FirstOrDefault(c => c.Name == "AvatarSize");
                    if (previewIconSize != null)
                    {
                        PreviewCreator.CreateAndSavePreview(qqfile.InputStream, new Size(previewIconSize.Width, previewIconSize.Height), Server.MapPath(filePreviewPath));
                    }
return Json(new
                    {
                        success = true,
                        result = "error",
                        data = new
                        {
                            filePath,
                            filePreviewPath
                        }
                    });


Запускаем. Загружаем. Файлы должны загрузиться, и создастся превью.
Теперь сделаем обработку в file-index.js (/Scripts/default/file-index.js):
$('#UploadImage').fineUploader({
            request: {
                endpoint: _this.ajaxFileUpload
            },
        })
        .on('error', function (event, id, name, reason) {
            //do something
        })
        .on('complete', function (event, id, name, responseJSON) {
          $("#ImagePreview").attr("src", responseJSON.data.filePreviewPath);
        });


теперь наш файл загружается вместе с превью. Путь большого файла также можно передавать отдельно, и записывать, например, в hidden поле и сохранять в дальнейшем в БД как строку.
Что плохого в такой конструкции, так это две следующие проблемы:
  • файлы могут быть перезаписаны, но это решается тем, что можно брать только расширение, а имя файлу присваивать отдельно, или добавлять немного соли
  • файлы могут быть загружены и не связаны с БД. Это можно решить тем, что для каждой таблице файлы записывать в отдельную папку, а потом делать поиск и удалять не записанные.


Получение файлов по ссылке

Есть еще один метод загрузки файла. Файл свободно болтается в интернете, а мы указываем путь к нему (например, при авторизации с facebook), а мы уже по ссылке сохраняем этот файл.
Это делается так:
var webClient = new WebClient();
var bytes = webClient.DownloadData(url);
var ms = new MemoryStream(bytes);


Где url – путь к файлу. Можно сложнее, с использованием HttpWebRequest:
public ActionResult Export(string uri)
        {

            HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(uri);
            webRequest.Method = "GET";
            webRequest.KeepAlive = false;
            webRequest.PreAuthenticate = false;
            webRequest.Timeout = 1000;
            var response = webRequest.GetResponse();

            var stream = response.GetResponseStream();
            var previewIconSize = Config.IconSizes.FirstOrDefault(c => c.Name == "AvatarSize");
            var filePreviewPath = Path.Combine("/Content/files/previews", Guid.NewGuid().ToString("N") + ".jpg");
                   
            if (previewIconSize != null)
            {
                PreviewCreator.CreateAndSavePreview(stream, new Size(previewIconSize.Width, previewIconSize.Height), Server.MapPath(filePreviewPath));
            }

            return Content("OK");
        }


Тут файл задается через генерацию Guid.NewGuid. Проверяем:
http://localhost/File/Export?uri=https://st.free-lance.ru/users/chernikov/upload/sm_f_81850beffd0d0c89.jpg

Файл загрузился и обработан. Всё супер!

Рекомендую пройтись дебаггером по работе PreviewCreator, чтобы понять, как там всё устроено.

Все исходники находятся по адресу https://bitbucket.org/chernikov/lessons
Читать дальше
Twitter
Одноклассники
Мой Мир

материал с habrahabr.ru

0

      Add

      You can create thematic collections and keep, for instance, all recipes in one place so you will never lose them.

      No images found
      Previous Next 0 / 0
      500
      • Advertisement
      • Animals
      • Architecture
      • Art
      • Auto
      • Aviation
      • Books
      • Cartoons
      • Celebrities
      • Children
      • Culture
      • Design
      • Economics
      • Education
      • Entertainment
      • Fashion
      • Fitness
      • Food
      • Gadgets
      • Games
      • Health
      • History
      • Hobby
      • Humor
      • Interior
      • Moto
      • Movies
      • Music
      • Nature
      • News
      • Photo
      • Pictures
      • Politics
      • Psychology
      • Science
      • Society
      • Sport
      • Technology
      • Travel
      • Video
      • Weapons
      • Web
      • Work
        Submit
        Valid formats are JPG, PNG, GIF.
        Not more than 5 Мb, please.
        30
        surfingbird.ru/site/
        RSS format guidelines
        500
        • Advertisement
        • Animals
        • Architecture
        • Art
        • Auto
        • Aviation
        • Books
        • Cartoons
        • Celebrities
        • Children
        • Culture
        • Design
        • Economics
        • Education
        • Entertainment
        • Fashion
        • Fitness
        • Food
        • Gadgets
        • Games
        • Health
        • History
        • Hobby
        • Humor
        • Interior
        • Moto
        • Movies
        • Music
        • Nature
        • News
        • Photo
        • Pictures
        • Politics
        • Psychology
        • Science
        • Society
        • Sport
        • Technology
        • Travel
        • Video
        • Weapons
        • Web
        • Work

          Submit

          Thank you! Wait for moderation.

          Тебе это не нравится?

          You can block the domain, tag, user or channel, and we'll stop recommend it to you. You can always unblock them in your settings.

          • habrahabr.ru
          • домен habrahabr.ru

          Get a link

          Спасибо, твоя жалоба принята.

          Log on to Surfingbird

          Recover
          Sign up

          or

          Welcome to Surfingbird.com!

          You'll find thousands of interesting pages, photos, and videos inside.
          Join!

          • Personal
            recommendations

          • Stash
            interesting and useful stuff

          • Anywhere,
            anytime

          Do we already know you? Login or restore the password.

          Close

          Add to collection

             

            Facebook

            Ваш профиль на рассмотрении, обновите страницу через несколько секунд

            Facebook

            К сожалению, вы не попадаете под условия акции