Pobierz plik Excel za pomocą AJAX MVC

Mam dużą (ish) formę w MVC.

Muszę być w stanie wygenerować plik excel zawierający dane z podzbioru tego formularza.

Najtrudniejsze jest to, że nie powinno to wpływać na resztę formularza, więc chcę to zrobić przez AJAX. Natknąłem się na kilka pytań, które wydają się być powiązane, ale nie mogę do końca zrozumieć, co oznaczają odpowiedzi.

Ten wydaje mi się najbliższy temu, czego szukam: asp-net-mvc-download-excel - ale nie jestem pewien, czy Rozumiem odpowiedź, a ma już kilka lat. Natknąłem się również na inny artykuł (nie mogę go znaleźć) o użyciu iframe do obsługi pobierania plików, ale nie jestem pewien, jak to działa z MVC.

Mój plik excel zwraca dobrze, jeśli robię pełny post z powrotem, ale nie mogę go pracować z AJAX w mvc.

Author: Community, 2013-05-21

11 answers

Nie można bezpośrednio zwrócić pliku do pobrania za pomocą wywołania AJAX, więc alternatywnym podejściem jest użycie wywołania AJAX do publikowania powiązanych danych na serwerze. Następnie możesz użyć kodu po stronie serwera, aby utworzyć plik Excel (polecam użycie EPPlus lub NPOI do tego, chociaż brzmi to tak, jakby ta część działała).

Aktualizacja wrzesień 2016

Moja oryginalna odpowiedź (poniżej) miała ponad 3 lata, więc pomyślałem, że zaktualizuję, ponieważ już nie tworzenie plików na serwerze podczas pobierania plików przez AJAX jednak pozostawiłem oryginalną odpowiedź, ponieważ może ona być w pewnym stopniu przydatna, w zależności od konkretnych wymagań.

Powszechnym scenariuszem w moich aplikacjach MVC jest raportowanie za pośrednictwem strony internetowej, która ma skonfigurowane przez użytkownika parametry raportu (zakresy dat, filtry itp.). Gdy użytkownik określi parametry, które zamieszcza na serwerze, generowany jest raport (np. plik Excel jako wyjście), a następnie zapisuję plik wynikowy jako tablica bajtów w zasobniku TempData z unikalnym odniesieniem. To odniesienie jest przekazywane z powrotem jako wynik Json do mojej funkcji AJAX, która następnie przekierowuje do oddzielnej akcji kontrolera, aby wyodrębnić dane z TempData i pobrać do przeglądarki użytkowników końcowych.

Aby podać więcej szczegółów, zakładając, że masz widok MVC, który ma formę związaną z klasą modelu, nazwijmy Model ReportVM.

Najpierw wymagane jest działanie kontrolera, aby otrzymać opublikowany model, przykład będzie:

public ActionResult PostReportPartial(ReportVM model){

   // Validate the Model is correct and contains valid data
   // Generate your report output based on the model parameters
   // This can be an Excel, PDF, Word file - whatever you need.

   // As an example lets assume we've generated an EPPlus ExcelPackage

   ExcelPackage workbook = new ExcelPackage();
   // Do something to populate your workbook

   // Generate a new unique identifier against which the file can be stored
   string handle = Guid.NewGuid().ToString();

   using(MemoryStream memoryStream = new MemoryStream()){
        workbook.SaveAs(memoryStream);
        memoryStream.Position = 0;
        TempData[handle] = memoryStream.ToArray();
   }      

   // Note we are returning a filename as well as the handle
   return new JsonResult() { 
         Data = new { FileGuid = handle, FileName = "TestReportOutput.xlsx" }
   };

}

Wywołanie AJAX, które wysyła mój formularz MVC do powyższego kontrolera i odbiera odpowiedź wygląda tak:

$ajax({
    cache: false,
    url: '/Report/PostReportPartial',
    data: _form.serialize(), 
    success: function (data){
         var response = JSON.parse(data);
         window.location = '/Report/Download?fileGuid=' + response.FileGuid 
                           + '&filename=' + response.FileName;
    }
})

Akcja kontrolera do obsługi pobierania pliku:

[HttpGet]
public virtual ActionResult Download(string fileGuid, string fileName)
{   
   if(TempData[fileGuid] != null){
        byte[] data = TempData[fileGuid] as byte[];
        return File(data, "application/vnd.ms-excel", fileName);
   }   
   else{
        // Problem - Log the error, generate a blank file,
        //           redirect to another controller action - whatever fits with your application
        return new EmptyResult();
   }
}

Inną zmianą, która może być łatwo przyjęta w razie potrzeby, jest przekazanie typu MIME pliku jako Trzeciego parametru, tak aby jedna akcja kontrolera mogła poprawnie obsługiwać różne formaty plików wyjściowych.

Usuwa to potrzebę jakiegokolwiek fizycznego pliki tworzone i przechowywane na serwerze, więc nie wymaga procedur porządkowych i po raz kolejny jest to bezproblemowe dla użytkownika końcowego.

Uwaga, zaletą używania TempData zamiast Session jest to, że po odczytaniu TempData Dane są czyszczone, dzięki czemu będą bardziej efektywne pod względem wykorzystania pamięci, jeśli masz dużą ilość żądań plików. Zobacz Najlepsze Praktyki TempData .

Oryginalna odpowiedź

Nie można bezpośrednio zwrócić pliku do pobrania poprzez wywołanie AJAX tak, alternatywnym podejściem jest użycie wywołania AJAX, aby opublikować powiązane dane na serwerze. Następnie możesz użyć kodu po stronie serwera, aby utworzyć plik Excel (polecam użycie EPPlus lub NPOI do tego, chociaż brzmi to tak, jakby ta część działała).

Po utworzeniu pliku na serwerze przekaż z powrotem ścieżkę do pliku (lub tylko nazwę pliku) jako wartość zwracaną do wywołania AJAX, a następnie Ustaw JavaScript window.location na ten adres URL, który wyświetli monit o przeglądarka do pobrania pliku.

Z punktu widzenia użytkowników końcowych operacja pobierania plików jest bezproblemowa, ponieważ nigdy nie opuszczają strony, na której pochodzi żądanie.

Poniżej znajduje się prosty, wymyślony przykład wywołania ajax, aby to osiągnąć:

$.ajax({
    type: 'POST',
    url: '/Reports/ExportMyData', 
    data: '{ "dataprop1": "test", "dataprop2" : "test2" }',
    contentType: 'application/json; charset=utf-8',
    dataType: 'json',
    success: function (returnValue) {
        window.location = '/Reports/Download?file=' + returnValue;
    }
});
  • URL parametr jest metodą kontrolera / akcji, w której kod utworzy plik Excel.
  • data parametr zawiera dane json, które zostaną wyodrębnione z forma.
  • returnValue będzie nazwą pliku nowo utworzonego pliku Excel.
  • okno .polecenie location przekierowuje do metody Controller / Action, która zwraca plik do pobrania.

Przykładową metodą kontrolera dla akcji pobierania będzie:

[HttpGet]
public virtual ActionResult Download(string file)
{   
  string fullPath = Path.Combine(Server.MapPath("~/MyFiles"), file);
  return File(fullPath, "application/vnd.ms-excel", file);
}
 156
Author: CSL,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-23 11:49:32

Moje 2 centy - nie musisz przechowywać Excela jako fizycznego pliku na serwerze-zamiast tego przechowuj go w pamięci podręcznej (sesji). Użyj unikalnie Wygenerowanej nazwy zmiennej pamięci podręcznej (przechowującej plik excel) - będzie to powrót (początkowego) wywołania ajax. W ten sposób nie musisz zajmować się problemami z dostępem do plików, zarządzaniem (usuwaniem) plikami, gdy nie są potrzebne itp. i, mając plik w pamięci podręcznej, jest szybsze, aby go odzyskać.

 19
Author: Luchian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-25 11:42:18

Ostatnio udało mi się to osiągnąć w MVC (chociaż nie było potrzeby używania Ajaxu) bez tworzenia fizycznego pliku i pomyślałem, że podzielę się swoim kodem:

Super prosta funkcja JavaScript (datatables.net button click wyzwala to):

function getWinnersExcel(drawingId) {
    window.location = "/drawing/drawingwinnersexcel?drawingid=" + drawingId;
}

Kod kontrolera C#:

    public FileResult DrawingWinnersExcel(int drawingId)
    {
        MemoryStream stream = new MemoryStream(); // cleaned up automatically by MVC
        List<DrawingWinner> winnerList = DrawingDataAccess.GetWinners(drawingId); // simple entity framework-based data retrieval
        ExportHelper.GetWinnersAsExcelMemoryStream(stream, winnerList, drawingId);

        string suggestedFilename = string.Format("Drawing_{0}_Winners.xlsx", drawingId);
        return File(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml", suggestedFilename);
    }

W klasie ExportHelper używam narzędzia 3rd party (GemBox.Arkusz kalkulacyjny ) do generowania pliku Excel i ma opcję Zapisz do strumienia. To powiedziawszy, istnieje wiele sposobów, aby twórz pliki Excel, które można łatwo zapisać w strumieniu pamięci.

public static class ExportHelper
{
    internal static void GetWinnersAsExcelMemoryStream(MemoryStream stream, List<DrawingWinner> winnerList, int drawingId)
    {

        ExcelFile ef = new ExcelFile();

        // lots of excel worksheet building/formatting code here ...

        ef.SaveXlsx(stream);
        stream.Position = 0; // reset for future read

     }
}

W IE, Chrome i Firefox przeglądarka wyświetla monit o pobranie pliku i nie ma rzeczywistej nawigacji.

 10
Author: Andy S,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-10-02 03:49:54

Użyłem rozwiązania opublikowanego przez CSL, ale polecam nie przechowywać danych pliku w sesji podczas całej sesji. Za pomocą TempData dane Pliku są automatycznie usuwane po następnym żądaniu (które jest żądaniem GET dla pliku). Możesz również zarządzać usuwaniem danych pliku w sesji w akcji pobierania.

Sesja może zużywać dużo pamięci/miejsca w zależności od miejsca na dysku SessionState i ilości plików eksportowanych podczas sesji oraz czy masz wiele użytkowników.

Zaktualizowałem kod strony serer z CSL, aby zamiast tego użyć TempData.

public ActionResult PostReportPartial(ReportVM model){

   // Validate the Model is correct and contains valid data
   // Generate your report output based on the model parameters
   // This can be an Excel, PDF, Word file - whatever you need.

   // As an example lets assume we've generated an EPPlus ExcelPackage

   ExcelPackage workbook = new ExcelPackage();
   // Do something to populate your workbook

   // Generate a new unique identifier against which the file can be stored
   string handle = Guid.NewGuid().ToString()

   using(MemoryStream memoryStream = new MemoryStream()){
        workbook.SaveAs(memoryStream);
        memoryStream.Position = 0;
        TempData[handle] = memoryStream.ToArray();
   }      

   // Note we are returning a filename as well as the handle
   return new JsonResult() { 
         Data = new { FileGuid = handle, FileName = "TestReportOutput.xlsx" }
   };

}

[HttpGet]
public virtual ActionResult Download(string fileGuid, string fileName)
{   
   if(TempData[fileGuid] != null){
        byte[] data = TempData[fileGuid] as byte[];
        return File(data, "application/vnd.ms-excel", fileName);
   }   
   else{
        // Problem - Log the error, generate a blank file,
        //           redirect to another controller action - whatever fits with your application
        return new EmptyResult();
   }
}
 5
Author: Niclas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-22 15:55:55

Najpierw Utwórz akcję kontrolera, która utworzy plik Excel

[HttpPost]
public JsonResult ExportExcel()
{
    DataTable dt = DataService.GetData();
    var fileName = "Excel_" + DateTime.Now.ToString("yyyyMMddHHmm") + ".xls";

    //save the file to server temp folder
    string fullPath = Path.Combine(Server.MapPath("~/temp"), fileName);

    using (var exportData = new MemoryStream())
    {
        //I don't show the detail how to create the Excel, this is not the point of this article,
        //I just use the NPOI for Excel handler
        Utility.WriteDataTableToExcel(dt, ".xls", exportData);

        FileStream file = new FileStream(fullPath, FileMode.Create, FileAccess.Write);
        exportData.WriteTo(file);
        file.Close();
    }

    var errorMessage = "you can return the errors in here!";

    //return the Excel file name
    return Json(new { fileName = fileName, errorMessage = "" });
}

Następnie utwórz akcję pobierania

[HttpGet]
[DeleteFileAttribute] //Action Filter, it will auto delete the file after download, 
                      //I will explain it later
public ActionResult Download(string file)
{
    //get the temp folder and file path in server
    string fullPath = Path.Combine(Server.MapPath("~/temp"), file);

    //return the file for download, this is an Excel 
    //so I set the file content type to "application/vnd.ms-excel"
    return File(fullPath, "application/vnd.ms-excel", file);
}

Jeśli chcesz usunąć plik po pobraniu Utwórz ten

public class DeleteFileAttribute : ActionFilterAttribute
{
    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        filterContext.HttpContext.Response.Flush();

        //convert the current filter context to file and get the file path
        string filePath = (filterContext.Result as FilePathResult).FileName;

        //delete the file after download
        System.IO.File.Delete(filePath);
    }
}

I wreszcie wywołanie ajax od Ciebie MVC Razor view

//I use blockUI for loading...
$.blockUI({ message: '<h3>Please wait a moment...</h3>' });    
$.ajax({
    type: "POST",
    url: '@Url.Action("ExportExcel","YourController")', //call your controller and action
    contentType: "application/json; charset=utf-8",
    dataType: "json",
}).done(function (data) {
    //console.log(data.result);
    $.unblockUI();

    //get the file name for download
    if (data.fileName != "") {
        //use window.location.href for redirect to download action for download the file
        window.location.href = "@Url.RouteUrl(new 
            { Controller = "YourController", Action = "Download"})/?file=" + data.fileName;
    }
});
 3
Author: Elvin Acevedo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-30 19:20:35

Ten wątek pomógł mi stworzyć własne rozwiązanie, które podzielę się tutaj. Używałem żądania GET ajax na początku bez problemów, ale dotarło to do punktu, w którym długość adresu URL żądania została przekroczona, więc musiałem przełączyć się na POST.

Javascript wykorzystuje wtyczkę do pobierania plików JQuery i składa się z 2 kolejnych wywołań. Jeden POST (aby wysłać params) i jeden dostać się do ponownego pliku.

 function download(result) {
        $.fileDownload(uri + "?guid=" + result,
        {
            successCallback: onSuccess.bind(this),
            failCallback: onFail.bind(this)
        });
    }

    var uri = BASE_EXPORT_METADATA_URL;
    var data = createExportationData.call(this);

    $.ajax({
        url: uri,
        type: 'POST',
        contentType: 'application/json',
        data: JSON.stringify(data),
        success: download.bind(this),
        fail: onFail.bind(this)
    });

Strona serwera

    [HttpPost]
    public string MassExportDocuments(MassExportDocumentsInput input)
    {
        // Save query for file download use
        var guid = Guid.NewGuid();
        HttpContext.Current.Cache.Insert(guid.ToString(), input, null, DateTime.Now.AddMinutes(5), Cache.NoSlidingExpiration);
        return guid.ToString();
    }

   [HttpGet]
    public async Task<HttpResponseMessage> MassExportDocuments([FromUri] Guid guid)
    {
        //Get params from cache, generate and return
        var model = (MassExportDocumentsInput)HttpContext.Current.Cache[guid.ToString()];
          ..... // Document generation

        // to determine when file is downloaded
        HttpContext.Current
                   .Response
                   .SetCookie(new HttpCookie("fileDownload", "true") { Path = "/" });

        return FileResult(memoryStream, "documents.zip", "application/zip");
    }
 0
Author: Machinegon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-11 16:20:04

Odpowiedź CSL została zaimplementowana w projekcie, nad którym pracuję, ale problem, który spowodowałem, polegał na skalowaniu na Azure, zepsuł nasze pobieranie plików. Zamiast tego udało mi się to zrobić jednym wywołaniem AJAX:

Serwer

[HttpPost]
public FileResult DownloadInvoice(int id1, int id2)
{
    //necessary to get the filename in the success of the ajax callback
    HttpContext.Response.Headers.Add("Access-Control-Expose-Headers", "Content-Disposition");

    byte[] fileBytes = _service.GetInvoice(id1, id2);
    string fileName = "Invoice.xlsx";
    return File(fileBytes, System.Net.Mime.MediaTypeNames.Application.Octet, fileName);
}

CLIENT (zmodyfikowana wersja pliku Pobierz Plik z ajax Post )

$("#downloadInvoice").on("click", function() {
    $("#loaderInvoice").removeClass("d-none");

    var xhr = new XMLHttpRequest();
    var params = [];
    xhr.open('POST', "@Html.Raw(Url.Action("DownloadInvoice", "Controller", new { id1 = Model.Id1, id2 = Model.Id2 }))", true);
    xhr.responseType = 'arraybuffer';
    xhr.onload = function () {
        if (this.status === 200) {
            var filename = "";
            var disposition = xhr.getResponseHeader('Content-Disposition');
            if (disposition && disposition.indexOf('attachment') !== -1) {
                var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
                var matches = filenameRegex.exec(disposition);
                if (matches != null && matches[1]) filename = matches[1].replace(/['"]/g, '');
            }
            var type = xhr.getResponseHeader('Content-Type');

            var blob = typeof File === 'function'
                ? new File([this.response], filename, { type: type })
                : new Blob([this.response], { type: type });
            if (typeof window.navigator.msSaveBlob !== 'undefined') {
                // IE workaround for "HTML7007: One or more blob URLs were revoked by closing the blob for which they were created. These URLs will no longer resolve as the data backing the URL has been freed."
                window.navigator.msSaveBlob(blob, filename);
            } else {
                var URL = window.URL || window.webkitURL;
                var downloadUrl = URL.createObjectURL(blob);

                if (filename) {
                    // use HTML5 a[download] attribute to specify filename
                    var a = document.createElement("a");
                    // safari doesn't support this yet
                    if (typeof a.download === 'undefined') {
                        window.location = downloadUrl;
                    } else {
                        a.href = downloadUrl;
                        a.download = filename;
                        document.body.appendChild(a);
                        a.click();
                    }
                } else {
                    window.location = downloadUrl;

                }

                setTimeout(function() {
                        URL.revokeObjectURL(downloadUrl);
                    $("#loaderInvoice").addClass("d-none");
                }, 100); // cleanup
            }
        }
    };
    xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
    xhr.send($.param(params));
});
 0
Author: wilsjd,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-10-05 20:08:32

Używając ClosedXML.Excel;

   public ActionResult Downloadexcel()
    {   
        var Emplist = JsonConvert.SerializeObject(dbcontext.Employees.ToList());
        DataTable dt11 = (DataTable)JsonConvert.DeserializeObject(Emplist, (typeof(DataTable)));
        dt11.TableName = "Emptbl";
        FileContentResult robj;
        using (XLWorkbook wb = new XLWorkbook())
        {
            wb.Worksheets.Add(dt11);
            using (MemoryStream stream = new MemoryStream())
            {
                wb.SaveAs(stream);
                var bytesdata = File(stream.ToArray(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "myFileName.xlsx");
                robj = bytesdata;
            }
        }


        return Json(robj, JsonRequestBehavior.AllowGet);
    }
 0
Author: G.V.K.RAO,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-10-06 13:40:20
$.ajax({
                type: "GET",
                url: "/Home/Downloadexcel/",
                contentType: "application/json; charset=utf-8",
                data: null,
                success: function (Rdata) {
                    debugger;
                    var bytes = new Uint8Array(Rdata.FileContents); 
                    var blob = new Blob([bytes], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
                    var link = document.createElement('a');
                    link.href = window.URL.createObjectURL(blob);
                    link.download = "myFileName.xlsx";
                    link.click();
                },
                error: function (err) {

                }

            });
 0
Author: G.V.K.RAO,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-10-06 14:19:41

Używam Asp.Net WebForm i po prostu chcę pobrać plik ze strony serwera. Jest wiele artykułów, ale nie mogę znaleźć tylko podstawowej odpowiedzi. Wypróbowałem podstawowy sposób I go dostałem.

To mój problem.

Muszę dynamicznie utworzyć wiele przycisków wejściowych w trybie runtime. I chcę dodać każdy przycisk do przycisku pobierania z podaniem unikalnego numeru pliku.

Tworzę każdy przycisk w ten sposób:

fragment += "<div><input type=\"button\" value=\"Create Excel\" onclick=\"CreateExcelFile(" + fileNumber + ");\" /></div>";

Każdy przycisk wywołuje ten ajax metoda.

$.ajax({
    type: 'POST',
    url: 'index.aspx/CreateExcelFile',
    data: jsonData,
    contentType: 'application/json; charset=utf-8',
    dataType: 'json',
    success: function (returnValue) {
      window.location = '/Reports/Downloads/' + returnValue.d;
    }
});

Potem napisałem podstawową prostą metodę.

[WebMethod]
public static string CreateExcelFile2(string fileNumber)
{
    string filePath = string.Format(@"Form_{0}.xlsx", fileNumber);
    return filePath;
}

Generuję formula_1, Formula_2, Formula_3.... I zamierzam usunąć te stare pliki innym programem. Ale jeśli istnieje sposób na wysłanie tablicy bajtów do pobrania pliku, jak za pomocą odpowiedzi. Chcę go użyć.

Mam nadzieję, że będzie to przydatne dla każdego.
 -1
Author: Can OTUR,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-25 06:26:18

On Submit form

public ActionResult ExportXls()
{   
 var filePath="";
  CommonHelper.WriteXls(filePath, "Text.xls");
}

 public static void WriteXls(string filePath, string targetFileName)
    {
        if (!String.IsNullOrEmpty(filePath))
        {
            HttpResponse response = HttpContext.Current.Response;
            response.Clear();
            response.Charset = "utf-8";
            response.ContentType = "text/xls";
            response.AddHeader("content-disposition", string.Format("attachment; filename={0}", targetFileName));
            response.BinaryWrite(File.ReadAllBytes(filePath));
            response.End();
        }
    }
 -1
Author: Rajesh Kumar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-07 06:08:25