Created
December 26, 2024 14:34
-
-
Save akrisanov/77468471e371da3f7ff1d367be05dc6b to your computer and use it in GitHub Desktop.
The application downloads a list of URLs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Net.Http; | |
namespace UrlDownloader; | |
public class Downloader | |
{ | |
private readonly string _downloadPath = "downloads"; | |
private readonly HttpClient _httpClient; | |
private readonly SemaphoreSlim _semaphore; | |
public Downloader(int maxConcurrentDownloads = 3) | |
{ | |
_semaphore = new SemaphoreSlim(maxConcurrentDownloads); | |
_httpClient = new HttpClient(); | |
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("CSharp-Downloader/1.0"); | |
// Ensure downloads directory exists | |
Directory.CreateDirectory(_downloadPath); | |
} | |
public async Task DownloadUrlsAsync(IEnumerable<string> urls) | |
{ | |
var downloadTasks = urls.Select(DownloadFileAsync); | |
await Task.WhenAll(downloadTasks); | |
Console.WriteLine("All downloads completed!"); | |
} | |
private async Task DownloadFileAsync(string url) | |
{ | |
try | |
{ | |
await _semaphore.WaitAsync(); | |
var fileName = GetFileName(url); | |
var filePath = Path.Combine(_downloadPath, fileName); | |
using var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); | |
response.EnsureSuccessStatusCode(); | |
await using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None); | |
await using var downloadStream = await response.Content.ReadAsStreamAsync(); | |
await downloadStream.CopyToAsync(fileStream); | |
Console.WriteLine($"Successfully downloaded: {url}"); | |
} | |
catch (Exception ex) | |
{ | |
Console.WriteLine($"Error downloading {url}: {ex.Message}"); | |
} | |
finally | |
{ | |
_semaphore.Release(); | |
} | |
} | |
private static string GetFileName(string url) | |
{ | |
var uri = new Uri(url); | |
var fileName = Path.GetFileName(uri.LocalPath); | |
return string.IsNullOrEmpty(fileName) | |
? "index.html" | |
: string.Join("_", fileName.Split(Path.GetInvalidFileNameChars())); | |
} | |
public void Dispose() | |
{ | |
_httpClient.Dispose(); | |
_semaphore.Dispose(); | |
} | |
} | |
public class Program | |
{ | |
public static async Task Main() | |
{ | |
var urls = new[] | |
{ | |
"https://golang.org/doc/gopher/frontpage.png", | |
"https://golang.org/doc/gopher/doc.png", | |
"https://golang.org/doc/gopher/pkg.png" | |
}; | |
await using var downloader = new Downloader(); | |
await downloader.DownloadUrlsAsync(urls); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"fmt" | |
"io" | |
"net/http" | |
"os" | |
"path" | |
"path/filepath" | |
"strings" | |
"sync" | |
) | |
func main() { | |
// List of URLs to download | |
urls := []string{ | |
"https://golang.org/doc/gopher/frontpage.png", | |
"https://golang.org/doc/gopher/doc.png", | |
"https://golang.org/doc/gopher/pkg.png", | |
} | |
// Create downloads directory if it doesn't exist | |
if err := os.MkdirAll("downloads", 0755); err != nil { | |
fmt.Printf("Error creating downloads directory: %v\n", err) | |
return | |
} | |
// Create a wait group to track goroutines | |
var wg sync.WaitGroup | |
// Create a semaphore to limit concurrent downloads | |
semaphore := make(chan struct{}, 3) // Limit to 3 concurrent downloads | |
// Process each URL | |
for _, url := range urls { | |
wg.Add(1) | |
go func(url string) { | |
defer wg.Done() | |
// Acquire semaphore | |
semaphore <- struct{}{} | |
defer func() { <-semaphore }() | |
if err := downloadFile(url); err != nil { | |
fmt.Printf("Error downloading %s: %v\n", url, err) | |
} else { | |
fmt.Printf("Successfully downloaded: %s\n", url) | |
} | |
}(url) | |
} | |
// Wait for all downloads to complete | |
wg.Wait() | |
fmt.Println("All downloads completed!") | |
} | |
func downloadFile(url string) error { | |
// Create HTTP client with timeout | |
client := &http.Client{} | |
// Create request | |
req, err := http.NewRequest("GET", url, nil) | |
if err != nil { | |
return fmt.Errorf("error creating request: %v", err) | |
} | |
// Add User-Agent header to avoid being blocked | |
req.Header.Set("User-Agent", "Go-Downloader/1.0") | |
// Get the data | |
resp, err := client.Do(req) | |
if err != nil { | |
return fmt.Errorf("error downloading: %v", err) | |
} | |
defer resp.Body.Close() | |
// Check server response | |
if resp.StatusCode != http.StatusOK { | |
return fmt.Errorf("bad status: %s", resp.Status) | |
} | |
// Create the file | |
fileName := getFileName(url) | |
filePath := filepath.Join("downloads", fileName) | |
out, err := os.Create(filePath) | |
if err != nil { | |
return fmt.Errorf("error creating file: %v", err) | |
} | |
defer out.Close() | |
// Write the body to file | |
_, err = io.Copy(out, resp.Body) | |
if err != nil { | |
return fmt.Errorf("error writing to file: %v", err) | |
} | |
return nil | |
} | |
func getFileName(url string) string { | |
// Extract filename from URL | |
fileName := path.Base(url) | |
// If the URL ends with a slash, use a default name | |
if fileName == "/" || fileName == "." { | |
fileName = "index.html" | |
} | |
// Clean the filename | |
fileName = strings.ReplaceAll(fileName, "?", "_") | |
fileName = strings.ReplaceAll(fileName, "&", "_") | |
return fileName | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import kotlinx.coroutines.* | |
import java.io.File | |
import java.net.URL | |
import java.nio.file.Files | |
import java.nio.file.Paths | |
class UrlDownloader { | |
private val downloadPath = "downloads" | |
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) | |
private val semaphore = kotlinx.coroutines.sync.Semaphore(permits = 3) // Limit concurrent downloads | |
init { | |
// Create downloads directory if it doesn't exist | |
Files.createDirectories(Paths.get(downloadPath)) | |
} | |
suspend fun downloadUrls(urls: List<String>) = coroutineScope { | |
val jobs = urls.map { url -> | |
launch { | |
semaphore.withPermit { | |
try { | |
downloadFile(url) | |
println("Successfully downloaded: $url") | |
} catch (e: Exception) { | |
println("Error downloading $url: ${e.message}") | |
} | |
} | |
} | |
} | |
jobs.joinAll() | |
println("All downloads completed!") | |
} | |
private suspend fun downloadFile(url: String) { | |
withContext(Dispatchers.IO) { | |
val connection = URL(url).openConnection() | |
connection.setRequestProperty("User-Agent", "Kotlin-Downloader/1.0") | |
val fileName = getFileName(url) | |
val filePath = File(downloadPath, fileName) | |
connection.getInputStream().use { input -> | |
filePath.outputStream().use { output -> | |
input.copyTo(output) | |
} | |
} | |
} | |
} | |
private fun getFileName(url: String): String { | |
val baseName = url.substringAfterLast('/') | |
return when { | |
baseName.isEmpty() -> "index.html" | |
else -> baseName.replace(Regex("[?&]"), "_") | |
} | |
} | |
} | |
suspend fun main() { | |
val urls = listOf( | |
"https://golang.org/doc/gopher/frontpage.png", | |
"https://golang.org/doc/gopher/doc.png", | |
"https://golang.org/doc/gopher/pkg.png" | |
) | |
val downloader = UrlDownloader() | |
downloader.downloadUrls(urls) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment