diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 27497976..aa32699c 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -3,7 +3,7 @@ name: CI/CD Pipeline on: [ push, pull_request, workflow_dispatch ] env: - AZURE_WEBAPP_NAME: devBetter + AZURE_WEBAPP_NAME: devbetter-linux AZURE_GROUP_NAME: DevBetterGroup AZURE_WEBAPP_PACKAGE_PATH: '.' @@ -12,7 +12,7 @@ jobs: name: Continuous Integration strategy: matrix: - os: [ ubuntu-latest, windows-latest ] + os: [ ubuntu-latest ] runs-on: ${{ matrix.os }} outputs: is_push_to_default_branch: ${{ steps.conditionals_handler.outputs.is_push_to_default_branch }} @@ -104,7 +104,7 @@ jobs: if: needs.ci.outputs.is_push_to_default_branch == 'true' name: Continuous Deployment needs: ci - runs-on: windows-latest + runs-on: ubuntu-latest steps: - name: Download publish artifacts id: dl_publish_artifacts diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index 3b1ebd9f..707f06b0 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -5,7 +5,7 @@ on: [pull_request, workflow_dispatch] jobs: build: - runs-on: windows-latest + runs-on: ubuntu-latest env: VIMEO_TOKEN: ${{ secrets.VIMEO_TOKEN }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a722e142..7aa6cdec 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,7 +1,7 @@ name: publish env: - AZURE_WEBAPP_NAME: devBetter + AZURE_WEBAPP_NAME: devbetter-linux AZURE_GROUP_NAME: DevBetterGroup AZURE_WEBAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root @@ -12,7 +12,7 @@ on: jobs: build-and-deploy: - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/Directory.Packages.props b/Directory.Packages.props index 5430aaa0..49736d2f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -62,7 +62,8 @@ - + + diff --git a/README.md b/README.md index acc36071..184cc6db 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A web application for devBetter.com, a developer coaching program web site and a ## What is devBetter? -Head over to [devBetter.com](https://devbetter.com) to see the live site. Scroll through the home page and read the testimonials. Essentially devBetter is a group dedicated to improving professional software developers of all stripes. We have a virtual community (currently using Discord) and we meet for live group Q&A sessions about once a week (currently using Zoom). We challenge and promote one another, answer tough code and software design questions, work through exercises, and more. This site is used as a playground by some members and its owner, Steve, to provide a real, working example of some of the coding techniques and practices we discuss. This is in contrast to labs, katas, and exercises that, while also valuable, are not the same as solving real world problems with real software in a production environment. +Head over to [devBetter.com](https://devbetter.com) to see the live site. Scroll through the home page and read the testimonials. Essentially devBetter is a group dedicated to improving professional software developers of all stripes. We have a virtual community (currently using Discord) and we meet for live group Q&A sessions about once a week (currently using Zoom). We challenge and promote one another, answer tough code and software design questions, work through exercises, and more. This site is used as a playground by some members and its owner, ardalis, to provide a real, working example of some of the coding techniques and practices we discuss. This is in contrast to labs, katas, and exercises that, while also valuable, are not the same as solving real world problems with real software in a production environment. ## Features diff --git a/global.json b/global.json index 9e0754e5..9ff1a348 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.0", + "version": "8.*", "rollForward": "latestFeature" } } \ No newline at end of file diff --git a/src/DevBetterWeb.Core/Services/CreateVideoService.cs b/src/DevBetterWeb.Core/Services/CreateVideoService.cs index 3fd70c40..bf487705 100644 --- a/src/DevBetterWeb.Core/Services/CreateVideoService.cs +++ b/src/DevBetterWeb.Core/Services/CreateVideoService.cs @@ -12,6 +12,8 @@ using NimblePros.Vimeo.Models; using NimblePros.Vimeo.VideoServices; using NimblePros.Vimeo.VideoTusService; +using static DevBetterWeb.Core.Entities.Member; +using static Microsoft.ApplicationInsights.MetricDimensionNames.TelemetryContext; namespace DevBetterWeb.Core.Services; public class CreateVideoService : ICreateVideoService @@ -20,13 +22,15 @@ public class CreateVideoService : ICreateVideoService private readonly IUploadVideoTusService _uploadVideoTusService; private readonly IRepository _repositoryArchiveVideo; private readonly IAddCreatedVideoToFolderService _addCreatedVideoToFolderService; + private readonly IAppLogger _logger; - public CreateVideoService(GetVideoService getVideoService, IUploadVideoTusService uploadVideoTusService, IRepository repositoryArchiveVideo, IAddCreatedVideoToFolderService addCreatedVideoToFolderService) + public CreateVideoService(IAppLogger logger, GetVideoService getVideoService, IUploadVideoTusService uploadVideoTusService, IRepository repositoryArchiveVideo, IAddCreatedVideoToFolderService addCreatedVideoToFolderService) { _getVideoService = getVideoService; _uploadVideoTusService = uploadVideoTusService; _repositoryArchiveVideo = repositoryArchiveVideo; _addCreatedVideoToFolderService = addCreatedVideoToFolderService; + _logger = logger; } public async Task StartAsync(string videoName, long videoSize, string domain, CancellationToken cancellationToken = default) @@ -40,9 +44,15 @@ public async Task StartAsync(string videoName, long videoSize, string do EmbedDomains = new List { domain }, HideFromVimeo = true }; - var sessionId = await _uploadVideoTusService.StartAsync(uploadVideoRequest, cancellationToken); + var responseSessionId = await _uploadVideoTusService.StartAsync(uploadVideoRequest, cancellationToken); + //TODO: Remove this + _logger.LogInformation($"Error Vimeo: {responseSessionId.Json}"); + if (!responseSessionId.IsSuccess || string.IsNullOrEmpty(responseSessionId.Data)) + { + _logger.LogError(new Exception(responseSessionId.Exception?.Message), responseSessionId.Json); + } - return sessionId; + return responseSessionId.Data; } public async Task UploadChunkAsync(bool isBaseFolder, string sessionId, string chunk, string? description, long? folderId, CancellationToken cancellationToken = default) diff --git a/src/DevBetterWeb.Web/DevBetterWeb.Web.csproj b/src/DevBetterWeb.Web/DevBetterWeb.Web.csproj index 20343cab..813e6a3b 100644 --- a/src/DevBetterWeb.Web/DevBetterWeb.Web.csproj +++ b/src/DevBetterWeb.Web/DevBetterWeb.Web.csproj @@ -48,6 +48,7 @@ + diff --git a/src/DevBetterWeb.Web/Endpoints/VideoEndpoints/UploadChunkEndpoint.cs b/src/DevBetterWeb.Web/Endpoints/VideoEndpoints/UploadChunkEndpoint.cs index 5833036b..c8628d9d 100644 --- a/src/DevBetterWeb.Web/Endpoints/VideoEndpoints/UploadChunkEndpoint.cs +++ b/src/DevBetterWeb.Web/Endpoints/VideoEndpoints/UploadChunkEndpoint.cs @@ -25,7 +25,7 @@ public UploadChunkEndpoint(ICreateVideoService createVideo, VimeoSettings vimeoS _vimeoSettings = vimeoSettings; } - [HttpPost("videos/upload")] + [HttpPost("api/videos/upload")] public override async Task> HandleAsync([FromBody] UploadChunkRequest uploadChunkRequest, CancellationToken cancellationToken = default) { if (uploadChunkRequest.Chunk.Length <= 0) diff --git a/src/DevBetterWeb.Web/Endpoints/VideoEndpoints/UploadVideoStartEndpoint.cs b/src/DevBetterWeb.Web/Endpoints/VideoEndpoints/UploadVideoStartEndpoint.cs index 283bcc10..748e6c95 100644 --- a/src/DevBetterWeb.Web/Endpoints/VideoEndpoints/UploadVideoStartEndpoint.cs +++ b/src/DevBetterWeb.Web/Endpoints/VideoEndpoints/UploadVideoStartEndpoint.cs @@ -14,15 +14,19 @@ public class UploadVideoStartEndpoint : EndpointBaseAsync .WithResult> { private readonly ICreateVideoService _createVideo; + private readonly IAppLogger _logger; - public UploadVideoStartEndpoint(ICreateVideoService createVideo) + public UploadVideoStartEndpoint(ICreateVideoService createVideo, + IAppLogger logger) { _createVideo = createVideo; + _logger = logger; } - [HttpPost("videos/start")] + [HttpPost("api/videos/start")] public override async Task> HandleAsync([FromBody] UploadVideoStartRequest request, CancellationToken cancellationToken = default) { + _logger.LogWarning("HandleAsync called for videos/start"); string domain = HttpContext.Request.Host.Value; var sessionId = await _createVideo.StartAsync(request.VideoName, request.VideoSize, domain, cancellationToken); diff --git a/src/DevBetterWeb.Web/Pages/Admin/Videos/Create.cshtml b/src/DevBetterWeb.Web/Pages/Admin/Videos/Create.cshtml index cf731d83..ba696d2d 100644 --- a/src/DevBetterWeb.Web/Pages/Admin/Videos/Create.cshtml +++ b/src/DevBetterWeb.Web/Pages/Admin/Videos/Create.cshtml @@ -2,21 +2,21 @@ @model DevBetterWeb.Web.Pages.Admin.Videos.CreateModel @{ - ViewData["Title"] = "Create"; + ViewData["Title"] = "Create"; } @@ -25,35 +25,35 @@
-
-
-
- - - -
-
-
-
- - -
-
-
-
-
-
- - -
-
-
-
- - -
-
-
+
+
+
+ + + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
@@ -71,30 +71,30 @@
-
-
-
-
-
- - 0% - -
-
-
-
-
-
- -
-
-
+
+
+
+
+
+ + 0% + +
+
+
+
+
+
+ +
+
+
@section Scripts { @@ -103,212 +103,214 @@ } -} + const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB + + function arrayBufferToBase64(buffer) { + let binary = ''; + const bytes = new Uint8Array(buffer); + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + return window.btoa(binary); + } + + $(document).ready(function () { + + if (window.FileReader) { + $('#videoFile').on('change', function (e) { + if (e.target.files == undefined || e.target.files.length <= 0) { + $('#videoName').val(''); + } else { + var fileName = e.target.files[0].name; + $('#videoName').val(fileName.split('.')[0]); + var creationDate = new Date(e.target.files[0].lastModified); + const formattedDate = creationDate.toISOString().slice(0, 10); + $('#videoCreationDate').val(formattedDate); + } + }); + $('#mdFile').on('change', function (e) { + var file = e.target.files[0]; + var reader = new FileReader(); + reader.onload = function (e) { + var data = reader.result; + $('#description').val(data); + updateMdPreview(data); + } + reader.readAsText(file); + }); + } + + document.querySelector("#confirmCreate").addEventListener("click", function (e) { + var videoFileInput = document.getElementById("videoFile"); + if (!videoFileInput.files.length) { + alert("Please select a video file."); + return false; + } + + const mdFileInput = document.getElementById("mdFile"); + if (!mdFileInput.files.length) { + alert("Please select an MD file."); + return false; + } + + const videoCreationDateInput = document.getElementById("videoCreationDate"); + const file = videoFileInput.files[0]; + const videoCreationDate = videoCreationDateInput.valueAsDate || new Date(file.lastModified); + + const videoNameInput = document.getElementById("videoName"); + const videoName = videoNameInput.value || file.name; + + if (file) { + startUpload(file, videoName, videoCreationDate); + } + }); + }); + function startUpload(file, videoName, videoCreationDate) { + const videoSize = file.size; + + const uploadVideoStartRequest = { + videoSize: videoSize, + videoName: videoName, + createdTime: videoCreationDate, + }; + + fetch('/api/videos/start', { + method: 'POST', + body: JSON.stringify(uploadVideoStartRequest), + headers: { + 'Content-Type': 'application/json', + 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value + } + }) + .then(response => { + if (!response.ok) { + document.getElementById('confirmCreate').disabled = false; + throw new Error(`HTTP error! status: ${response.status}`); + } else { + return response.text(); + } + }) + .then(data => { + try { + let jsonData = JSON.parse(data); + uploadChunks(jsonData.sessionId, file, null); + } catch (e) { + document.getElementById('confirmCreate').disabled = false; + console.log("The server's response wasn't valid JSON. It was:", data); + console.log("The server's response wasn't valid JSON. It was:", e); + + } + }) + .catch(error => { + document.getElementById('confirmCreate').disabled = false; + console.error('Request failed', error); + alert(error.message); + }); + } + + function uploadChunks(sessionId, file, folderId) { + let offset = 0; + + function nextChunk() { + if (offset < file.size) { + const chunk = file.slice(offset, offset + CHUNK_SIZE); + let isLastChunk = offset + CHUNK_SIZE >= file.size; + uploadChunk(sessionId, chunk, isLastChunk, isLastChunk ? folderId : null) + .then(data => { + if (data == 2) { //uploadComplete + document.getElementById('confirmCreate').disabled = false; + } + offset += CHUNK_SIZE; + + let progress = Math.min(100, (offset / file.size) * 100).toFixed(2); + updateProgressBar(progress); + + nextChunk(); + }) + .catch(error => { + console.error('Request failed', error); + document.getElementById('confirmCreate').disabled = false; + alert(error.message); + }); + } else { + } + } + + nextChunk(); + } + + function updateProgressBar(progress) { + let progressBar = document.getElementById("uploadProgressBar"); + let progressText = document.getElementById("progressText"); + progressBar.style.width = progress + "%"; + progressBar.setAttribute("aria-valuenow", progress); + progressText.innerText = progress + "%"; + } + + function uploadChunk(sessionId, chunk, isLastChunk, folderId) { + return new Promise((resolve, reject) => { + mdFileContentPromise = readMdFilePromise(); + + const reader = new FileReader(); + reader.onload = async function (e) { + const base64Chunk = btoa( + new Uint8Array(e.target.result) + .reduce((data, byte) => data + String.fromCharCode(byte), '') + ); + let body = { sessionId, chunk: base64Chunk }; + if (folderId !== null) { + body.folderId = folderId; + } + if (isLastChunk) { + const mdFileContent = await mdFileContentPromise; + body.description = mdFileContent; + } + fetch('/api/videos/upload', { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json', + 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value + } + }) + .then(response => { + if (!response.ok) { + throw new Error('Upload chunk failed'); + } + resolve(response.json()); + }) + .catch(reject); + }; + + reader.onerror = function () { + reject(new Error('Failed to read file')); + }; + + reader.readAsArrayBuffer(chunk); + }); + } + + function updateMdPreview(descriptionContent) { + var converter = new showdown.Converter(); + var htmlContent = converter.makeHtml(descriptionContent); + + $('#mdPreview').html(htmlContent); + } + + function readMdFilePromise() { + const mdFileInput = document.getElementById("mdFile"); + const mdFile = mdFileInput.files[0]; + const mdFileReader = new FileReader(); + const mdFileContentPromise = new Promise((resolve) => { + mdFileReader.onload = (e) => { + resolve(e.target.result); + }; + mdFileReader.readAsText(mdFile); + }); + + return mdFileContentPromise; + } + + + +} \ No newline at end of file diff --git a/src/DevBetterWeb.Web/Program.cs b/src/DevBetterWeb.Web/Program.cs index f0af55f2..b8550a02 100644 --- a/src/DevBetterWeb.Web/Program.cs +++ b/src/DevBetterWeb.Web/Program.cs @@ -175,7 +175,7 @@ } app.UseHttpsRedirection(); -app.UseStaticFiles(); + //app.UseCookiePolicy(); app.UseRouting(); @@ -193,8 +193,12 @@ } app.MapRazorPages(); + +app.UseStaticFiles(); app.MapDefaultControllerRoute(); + + // seed database await ApplyLocalMigrationsAsync(app); await SeedDatabase(app); diff --git a/src/DevBetterWeb.Web/web.config~f19ee8c3400f98bb3eb9977df0c24b3ae0b438c0 b/src/DevBetterWeb.Web/web.config~f19ee8c3400f98bb3eb9977df0c24b3ae0b438c0 deleted file mode 100644 index 2e6d7f8a..00000000 --- a/src/DevBetterWeb.Web/web.config~f19ee8c3400f98bb3eb9977df0c24b3ae0b438c0 +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - -