استریم فایل در فونیکس

با درود خدمت دوستان و استاید محترم.

نیازمندی به دانلود فایل به صورت استریمی به شرح بود. اول نیاز داشتم که وقتی فایلی را برای دانلود قرار می دهم کاربر نتواند مستقیم دانلود کند بلکه اسم فایل نیز تغییر کند در بین کل کد هایی که در اینترنت بود و راه حل های مختلف این چند خط کد نیاز بنده را مرتفع نمود.

  def download(conn, _params) do
    file = File.read!("/test123365.png")


    conn
     |> put_resp_content_type("image/png")
     |> put_resp_header("Content-disposition","attachment; filename=\"test.png\"")
     |> put_resp_header("X-Accel-Redirect", "/tempfile/download/test.png")
     |> put_resp_header("Content-Type", "application/octet-stream")
     |> send_resp(200, file)
  end
end

خوب تا اینجا مشکل اول حل شد ولی مشکل دوم زمانی شروع شد که فایل های من بزرگ بودند و من می خواستم اون هارو چانک بندی کنم (chunk) و به صورت استریمینگ ارسال کنم که مشکلم دقیقا در همینجاست.

پست بنده در انجمن elixir


من یک مطلبی که لینک دادن بودن بهم رو هم دیدم به نام Very high memory usage when streaming file with Phoenix

def show(conn, _params) do
    url = "http://localhost:8080/Bigfile.mp4"
    %{headers: proxy_headers} = HTTPoison.head!(url)
    proxy_headers = for {k, v} <- proxy_headers, do: {String.downcase(k), v}, into: %{}

    chunked_conn =
      conn
      |> put_resp_content_type("video/mp4")
      |> put_resp_header("content-length", proxy_headers["content-length"])
      |> send_chunked(200)

    url
    |> HttpStream.stream()
    |> Stream.map(fn n -> chunk(chunked_conn, n) end)
    |> Stream.run()
  end

این قطعه کد که نمی دونم HttpStream رو از کجا گرفته !! فکر می کنم برای لود کردن فایل از جای دیگه ای است تا فایل برای دانلود آماده کردن .

به هر حال بنده هنوز نتونستم به صورت چانک های سفارشی که خودم می خواستم این فایل هارو به کاربرم ارسال کنم

با تشکر


از ماژول بالا این لینک گیتهاب رو پیدا کرده بودم

این توضیحات بخون

بجای

File.read

از
stream
استفاده کن

https://hexdocs.pm/elixir/File.Stream.html

https://www.poeticoding.com/processing-large-csv-files-with-elixir-streams/

https://www.poeticoding.com/elixir-stream-and-large-http-responses-processing-text/

1 Like

درود سام عزیز. متاسفانه چندین بار این مطالب رو خوندم کل امروز روی این موضوع بودم مشکل اصلی اینکه فایل هامو نمی تونم پارت به پارت کنم یا chunk کنم بعد بفرستمش.

بجای File.read از خود File.stream‍ هم استفاده کردم اول اینکه کل اون خروجی رو نمی شه داد و بعد می شه مسیرشو داد یکی درمیون در فایل های بزرگ مثل اینکه عقب می افته هدر فونیکس یا … کار نمی کنه

به عنوان مثال من به گوگل به این صورت می فرستم

file = File.stream!("/Ghalam.png", [], 204800)
file |> Enum.with_index |> Enum.each(fn {content, _index} ->
****
 end)

در بالا content رو می فرستادم به گوگل که باینری بود ولی وقتی اینو به |> send_resp(200, content) می دم ارور

no function clause matching in Plug.Conn.resp/3

ارور بالا می ده . اکثر این آموزش ها بیشتر برای لود csv هست و چیزی به کاربر برای دانلود نمی فرستند و فایل های من همشون mp4, png یا پی دی اف هستند

مشکل اصلی من روی فانکشن فونیکس هست برای آماده سازی دانلود


از send_chunked
استفاده کردی مثل پست های بالا ?

1 Like

از این یک بار می خواستم استفاده کنم ولی موفق نشدم ازش استفاده کنم یک اروری داشتم ولی این پست ندیده بودم. خیلی کد نمونه کم هست تو اینترنت نمی دونم چرا ! :disappointed:

تو این پست هم فکر کنم داره فایل های چانک شده رو می گیره به کاربر که دانلود کنه نمی فرسته

ولی فکر کنم این پست مشکلمو شاید حل کنه فردا یک تست کامل می زنم اینجا باز نتیجه رو می زارم ممنون سام عزیز

من این کدو برای تصویر تست کردم پاسخگو بود و کار کرد

حالا مرحله بعدی اینکه ببنیم چطور می تونم چانک های مربوط به خودمو بسازم یعنی تعداد و سایز اونارو سفارشی سازی کنم.

ولی دیشب که یک پست مجدد زدم برای مشکل ارسال استریم فایل
در لینک زیر

این بزرگوار گفته که به صورت خودکار ارسال فایل از فونیکس خودش استریم هست ولی من در متنی که ایشون لینک کردن جایی ندیدم که بگه استریم هست فقط گفته انقدر می فرسته تا کانکشن تمام بشه و …

و کاری که من با کدی که در لینک استک بود انجام دادم در حقیقت یک کار اضافه داره . اول از یک آدرس فایل رو دانلود می کنه و قطعه قطعه می ده به کاربر . یعنی فایلی که روی هاست هست رو داره دوباره دانلود می کنه.

مثل اینکه این مطلب هم نشون دهنده یک باگ در کابوی ۲ هست

من یکمی کد هارو تغییر دادم

  def download(conn, _params) do
    # url = "http://localhost:4000/images/logo.png"
    # %HTTPoison.AsyncResponse{id: id} = HTTPoison.get!(url, %{}, stream_to: self())

    file_streamed = File.stream!("/Users/shahryar/Desktop/Khat-Ghalam.png", [], 200)
    conn = conn
    |> put_resp_content_type("image/event-stream")
    |> put_resp_header("Content-disposition","attachment; filename=\"test.png\"")
    |> put_resp_header("X-Accel-Redirect", "/tempfile/download/test.png")
    |> put_resp_header("Content-Type", "application/octet-stream")
    |> send_chunked(200)

    file_streamed
    |> Enum.with_index
    |> Enum.each(fn {content, _index} ->
      conn |> chunk(content)
    end)

    # process_httpoison_chunks(conn, id)
  end

کد کار می کنه دانلود اوکی می شه ولی متاسفانه یک ارور می ده اونم می گه plug conn رو فراخوانی کنیم که انجام شده ولی ارور داره

[error] #PID<0.3708.0> running KhatoghalamWeb.Endpoint (connection #PID<0.3707.0>, stream id 1) terminated
Server: localhost:4000 (http)
Request: GET /download
** (exit) an exception was raised:
    ** (RuntimeError) expected action/2 to return a Plug.Conn, all plugs must receive a connection (conn) and return a connection, got: :ok
        (khatoghalam) lib/khatoghalam_web/controllers/client_home_controller.ex:1: KhatoghalamWeb.ClientHomeController.phoenix_controller_pipeline/2

من احساس می کنم مسیری که در استریمینگ فایل وجود داره مخصوصا در فایل های غیر متنی مثل ویدیو اینا زیاد تاثیری نداره جز در دانلود اون به صورت استریمینگ که با لینکی که دادید انجام شدنی بود. فکر کنم روی فایل های csv و لود اونا مشکل زیاد نیست.

جالب اینجاست که این مشکلات فقط برای یادگیری هست و من نیاز ندارمشون ولی یکمی ترسیدم اگر بخوام در این سیستمم ویدیو رو استریم کنم چطور!!


به روز رسانی

قبل از end اومدم یک conn صدا کردم مشکل ارور حل شد دانلود می ده . حالا باید تست کنمش

مثلا فکر کنیم در بازی هستیم من بعد از هر بازی که شما می کنید بهتون یک بخش از این فایل رو می دم آیا کلاینت منتظر می مونه ؟

حالا مشکل جدیدی رو داریم که فکر می کنم مشکل آخر باشه. و از اونجایی که این تمرین برای شروع استریمنیگ ویدیو و نمایش و بافر کردنش هست

من اومدم برای اینکه

file_streamed = File.stream!("/Users/shahryar/Desktop/Khat-Ghalam.png", [], 200)
    conn = conn
    |> put_resp_content_type("image/event-stream")
    |> put_resp_header("Content-disposition","attachment; filename=\"test.png\"")
    |> put_resp_header("X-Accel-Redirect", "/tempfile/download/test.png")
    |> put_resp_header("Content-Type", "application/octet-stream")
    |> send_chunked(200)

    file_streamed
    |> Enum.with_index
    |> Enum.each(fn {content, _index} ->
      conn |> chunk(content)
      Process.sleep 2000
    end)
    conn

من اینجا یک Process.sleep 2000 قرار دادم ولی نرم افزاری که می خواد فایل رو دریافت کنه کانکشنش از بین می ره و متوقف می شه

The client will ever try to receive as fast as it can.
Unless you tell it to do otherwise.

حالا اینجا فکر کنم باید مسیر من عوض بشه !!! مثلا
من اگر بیام یک چانک فایل غیر ولید شده رو ارسال کنم !! یعنی این پارت از فایلم چیزی نباشه فقط کانشکن رو زنده نگهداره. حالا مشکل اینجاست چطور باید به مثلا idm طرف بفهمونم که این ارسال های غیر ولید فقط برای زنده نگهداشتنشه !! و اینا رو در نظر نگیره . و در آخر کل فایل هارو یکی کنه و خروجی دانلود نهایی بشه

@samdvr سام عزیز برای این کار نظری داری عزیز؟ یعنی مشکل آخر من

اصلا sleep نگذار تا زمانی که داده داره فرستاده میشه نباید قطع بشه timeout کانکشن چقدره؟ timeout درخواست چیه

1 Like

اسلیپ گزاشتم سام عزیز بخاطر این موضوع:

فکر کنید یک بازی هست هر رکواستی که می ببری در بازی یک پارت از فایل های چانک شده رو برای شما می فرسته. حالا مشکل اینجاست من نمی دونم کاربرم قرار است چه زمانی رکواست دوم یا سوم رو ببره ممکن یک دقیقه ممکنه ۲۰ دقیقه بکشه !!

در حالت استریمینگ نباید در مورد این فکر کنی http
به client میگه که chunk های دیگه مونده این سرور هست که تایین میکنه request ادامه داره یا تموم شده
Client فقط میخونه

مشکل همینجاست من نمی تونم به کلاینت بگم هنوز مونده و متوقف می شه :upside_down_face:

مثلا با یک دانلود منیجر انجام دادم منتظر مونده ولی تصویر آخر که تمام شد سیاه بوده فکر کنم باید پستشو در انجمن الکسیر ادامه بدم جون یک بخش دیگه هم مشکل داره
conn اخری که داره صدا زده می شه هنوز منتظر نمونده کل متادیتا آبدیت بشه

توی دانلود منیجر منتظر موند ولی نرم افزار rested که یک نرم افزار ارسال درخواست json هست اون متوقف شد فکر کنم مشکل از خود نرم افزار مذکور هست که برای کار دانلود درست نشده

نباید بگی طوری که http stream کارمیکنه اینه که هر تکه از response یک هدر transfer encoding
با مقدار chunked سرور میفرسته و client میدونه این stream و باید باز هم صبر کنه برای تکه های بعدی

ممنون پست بالا به روز کردم

به جای ۲۰۰ byte مقدار chunk ها رو زیاد کن اینجا

File.stream!("/Users/shahryar/Desktop/Khat-Ghalam.png", [], 200)
1 Like

مشکل اینجاست که conn کامل آبدیت نمی شه بهم داخل انجمن گفتند که از Enum.reduce بجای Enum.each استفاده کنم هنوز موفق نشدم. ازش استفاده کنم .
اینو بتونم راه اندازم دیگه فکر کنم Enum.reduce_while/3 رو می تونم بیارم بالا. که کامل تره انگار

در پست های بالا که مباحث زیادی مطرح شد در انجمن الکسیر نیز همینطور ولی گفتم اینم بزارم

https://hexdocs.pm/mint/Mint.HTTP.html#recv/3