Criando um sistema básico de Short Url (parte 2)
Depois de vários meses usando o sistema e tendo que atualizar a lista de links manualmente no XML via FTP, decidi que era hora de melhorar o sistema para que incluísse um banco de dados e um meio de incluir, editar e remover links online.
Portanto, melhorei o sistema e hoje está disponível em http://puul.ga
O código fonte (open source) está disponível no Github
Foram feitas várias melhorias no sistema, de forma a ser responsivo e fácil de usar. Mas seu visual e utilidades continuam simples. Cria-se um short link (mesmo conceito que bit.ly) que pode ser compartilhado em qualquer lugar, deixando o link curto e fácil de ser digitado.
O projeto continua sendo em C#, MVC, Entity Framework, Html5, Css3 e jQuery (Bootstrap)
Primeiramente, é necessário criar o objeto/classe que irá conter os dados do Link gerado:
using System; using System.ComponentModel.DataAnnotations; namespace Shorten_Urls.Models { public class Url { public int Id { get; set; } [RegularExpression(@"^.{8,}$", ErrorMessage = "Minimum 8 characters required")] [Required(ErrorMessage = "Required")] [StringLength(30, MinimumLength = 8, ErrorMessage = "Invalid")] [Display(Name ="Shareable Url")] public string Src { get; set; } [Url] [Display(Name = "Redirects To")] public string Redirect { get; set; } public string UserId { get; set; } [Display(Name = "Created By")] public string Username { get; set; } //[DataType(DataType.Date)] //[DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}", ApplyFormatInEditMode = true)] [Display(Name = "Created On")] public DateTime CreatedOn { get; set; } //[DataType(DataType.Date)] //[DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}", ApplyFormatInEditMode = true)] [Display(Name = "Expires")] public DateTime? Expires { get; set; } } }
Também é necessário criar o Repositório (ou Data Layer). Esse repositório permite o uso tanto de XML quanto SQL, sendo configurada a opção no
web.config
using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Microsoft.AspNet.Identity; using System.Configuration; using System.Data.Entity; namespace Shorten_Urls.Models { public class UrlRepository : IRepository<Url> { private XDocument _urls; private List<Url> urls; ApplicationDbContext _context; private readonly string DbProvider = ConfigurationManager.AppSettings["DbProvider"]; private const string SQL = "Sql"; private const string XML = "Xml"; public bool needLastId { get { return DbProvider.Equals(XML); } } public List<Url> Urls { get { return urls; } } public int LastId { get { return urls.Last().Id; } } public UrlRepository() { urls = new List<Url>(); if (DbProvider == XML) { _urls = MvcApplication.IO.Forwards; IEnumerable<XElement> rootDescendants = _urls.Descendants("url"); foreach (XElement e in rootDescendants) urls.Add(MapXElement(e)); } else if (DbProvider == SQL) { _context = new ApplicationDbContext(); urls = _context.Forwarders.ToList(); } } public bool Exists(string src) { var yes = from y in urls where y.Src == src select y; if (yes.Count() > 0) return true; else return false; } public IEnumerable<Url> GetAll() { return urls; } public void Add(Url url) { urls.Add(url); if (DbProvider == XML) MvcApplication.IO.AddForward(url); else if (DbProvider == SQL) { _context.Forwarders.Add(url); _context.SaveChanges(); } } public Url GetById(int Id) { Url e = (from url in urls where url.Id == Id select url).Single(); if (DbProvider == SQL) { PopulateUsername(e); } return e; } public List<Url> GetByUserId(string userId) { List<Url> list = (from urlList in urls where urlList.UserId == userId select urlList).ToList(); if (DbProvider == SQL) { foreach (Url url in list) PopulateUsername(url); } return list; } public Url GetBySrc(string src) { var e = (from url in urls where url.Src.Equals(src) select url); if (e.Count() == 0) return null; else { if (DbProvider == SQL) { PopulateUsername(e.Single()); } return e.Single(); } } public void Remove(int id) { Url remove = (from url in urls where url.Id == id select url).Single(); urls.Remove(remove); Remove(remove); } public void Remove(Url url) { urls.Remove(url); if (DbProvider == XML) MvcApplication.IO.RemoveForward(url); else if (DbProvider == SQL) { _context.Forwarders.Remove(url); _context.SaveChanges(); } } public void Remove(string src) { Url remove = (from url in urls where url.Src == src select url).Single(); urls.Remove(remove); Remove(remove); } public void RemoveAll() { urls.Clear(); } public void RemoveByUserId(string userId) { Url remove = (from url in urls where url.UserId == userId select url).Single(); urls.Remove(remove); Remove(remove); } public void Update(Url url) { Url old = (from urlE in urls where urlE.Id == url.Id select urlE).Single(); urls.Remove(old); urls.Add(url); if (DbProvider == XML) { MvcApplication.IO.RemoveForward(old); MvcApplication.IO.AddForward(url); } else if (DbProvider == SQL) { _context.Entry(url).State = EntityState.Modified; _context.SaveChanges(); } } private Url PopulateUsername(int id) { Url url = (from u in urls where u.Id == id select u).Single(); string userId = url.UserId == null ? "" : url.UserId; UserRepository userRepo = new UserRepository(); url.Username = userRepo.GetUsername(userId); return url; } private Url PopulateUsername(Url url) { UserRepository userRepo = new UserRepository(); string userId = url.UserId == null ? "" : url.UserId; url.Username = userRepo.GetUsername(userId); return url; } private Url MapXElement(XElement element) { DateTime expires = element.Attribute("expires").Value == "" ? new DateTime(2999, 12, 31) : DateTime.Parse(element.Attribute("expires").Value); DateTime createdOn = element.Attribute("created").Value == "" ? DateTime.Now : DateTime.Parse(element.Attribute("created").Value); string userId = element.Attribute("userid").Value; int id = int.Parse(element.Attribute("id").Value); Url url = new Url { Id = id, CreatedOn = createdOn, Expires = expires, UserId = userId, Redirect = element.Value.Trim(), Src = element.Attribute("src").Value }; return PopulateUsername(url); } } }
A parte mais importante do projeto é o Controller que vai servir os Links, e as rotas de leitura dos links. Portanto, o
RouteConfig.cs
também tem que estar correto:
ForwardsController.cs
using Shorten_Urls.Models; using System; using System.Collections.Generic; using System.Web.Mvc; using System.Web.Script.Serialization; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; namespace Shorten_Urls.Controllers { public class ForwardsController : Controller { private UrlRepository _repo; private UserRepository _usersRepo; private ApplicationUser user; public ForwardsController() { _repo = new UrlRepository(); _usersRepo = new UserRepository(); } // // GET: /Forwards/ public ActionResult SendToSrc(string src) { string error404 = "404.html"; string redirect = "/Home/Index"; Url url = _repo.GetBySrc(src); if (url != null) { if (!url.Redirect.Equals(string.Empty) && (url.Expires > DateTime.Now || url.Expires == null)) redirect = url.Redirect; else redirect = error404; } return Redirect(redirect); } [Authorize] public ActionResult List() { List<Url> urls; if (User != null) user = _usersRepo.GetById(User.Identity.GetUserId()); if (user.UserType == UserTypes.Administrator) urls = _repo.Urls; else urls = _repo.GetByUserId(user.Id); return View(urls); } // // GET: /Forwards/Details/5 [Authorize] public ActionResult Details(int id) { if (User != null) user = _usersRepo.GetById(User.Identity.GetUserId()); if (id == 0) Redirect("/Home/Index"); Url url = _repo.GetById(id); return View(url); } // // GET: /Forwards/Create [Authorize] public ActionResult Create() { if (User != null) user = _usersRepo.GetById(User.Identity.GetUserId()); Url url = new Url(); string random = Helpers.GenerateRandomgUrl(); while (_repo.Exists(random)) { random = Helpers.GenerateRandomgUrl(); } ViewBag.RandomUrl = random; return View(url); } [HttpGet] public string Available(string Src) { bool exists = _repo.Exists(Src); return new JavaScriptSerializer().Serialize(!exists); } // // POST: /Forwards/Create [Authorize] [HttpPost] public ActionResult Create(string Redirect, string Src, DateTime? Expires) { try { // TODO: Add insert logic here if (User != null) user = _usersRepo.GetById(User.Identity.GetUserId()); Url newUrl = new Url { Redirect = Redirect, Src = Src, CreatedOn = DateTime.Now, Expires = Expires, UserId = user.Id, Username = _usersRepo.GetUsername(user.Id) }; if (_repo.needLastId) newUrl.Id = _repo.LastId + 1; _repo.Add(newUrl); TempData["Success"] = "Short Url has been created"; return RedirectToAction("List"); } catch { TempData["Error"] = "There was an error saving your new Url"; return View("List"); } } // // GET: /Forwards/Edit/5 [Authorize] public ActionResult Edit(int id) { if (User != null) user = _usersRepo.GetById(User.Identity.GetUserId()); Url editUrl = _repo.GetById(id); return View(editUrl); } // // POST: /Forwards/Edit/5 [Authorize] [HttpPost] public ActionResult Edit(int Id, string Redirect, string Src, DateTime? Expires = null) { try { // TODO: Add update logic here if (User != null) user = _usersRepo.GetById(User.Identity.GetUserId()); Url editUrl = _repo.GetById(Id); editUrl.Redirect = Redirect; editUrl.Src = Src; editUrl.UserId = user.Id; editUrl.Expires = Expires; editUrl.Username = _usersRepo.GetUsername(user.Id); _repo.Update(editUrl); TempData["Success"] = "Short Url has been updated"; return RedirectToAction("List"); } catch { TempData["Error"] = "There was an error saving your Url"; return View(); } } // // GET: /Forwards/Delete/5 [Authorize] public ActionResult Delete(int id) { if (User != null) user = _usersRepo.GetById(User.Identity.GetUserId()); Url url = _repo.GetById(id); return View(url); } [Authorize] [HttpPost] public ActionResult Delete(int id, bool confirm) { try { if (User != null) user = _usersRepo.GetById(User.Identity.GetUserId()); _repo.Remove(id); TempData["Success"] = "Short Url has been deleted"; return RedirectToAction("List"); } catch { TempData["Error"] = "There was an error deleting your Url"; Url url = _repo.GetById(id); return View(url); } } } }
RouteConfig.cs
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; namespace Shorten_Urls { public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Forward", url: "{src}", defaults: new { controller = "Forwards", action = "SendToSrc", src = "" } ); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); } } }
Ao final, configura-se o
web.config
com as seguintes chaves:
<appsettings> <add key="ForwardsFile" value="~/App_Data/forwards.xml"></add> <add key="LenghtOfRandomString" value="8"></add> <add key="WebsiteName" value="Puulga"></add> <add key="WebsiteUrl" value="http://puul.ga/"></add> <add key="Website" value="true"></add> <!--email provider section--> <add key="siteadmin" value="email@puul.ga"></add> <add key="email-subject" value="Email from Puul.ga"></add> <add key="email-from" value="email@puul.ga"></add> <add key="email-from-name" value="Puul.ga"></add> <add key="email-require-auth" value="false"></add> <add key="email-username" value="puul.ga"></add> <add key="email-password" value="password"></add> <add key="email-port" value="3535"></add> <add key="useSSL" value="false"></add> <add key="email-server" value="localhost"></add> <add key="email-html-share" value="~/Email Templates/share.html"></add> <add key="email-html-default" value="~/Email Templates/puulga.html"></add> <add key="email-html-forgotten" value="~/Email Templates/puulga.html"></add> <add key="email-html-confirmation" value="~/Email Templates/puulga.html"></add> <add key="email-html-welcome" value="~/Email Templates/puulga.html"></add> <!--Database options. possible values: Xml, Sql--> <add key="DbProvider" value="Sql"></add> </appsettings>
Basicamente, esses são os elementos mais importantes para o funcionamento do projeto. Todo o código fonte está disponível no Github em código aberto.
Visite o site para ver como funciona.