Skip to content

Commit 468fc18

Browse files
committed
upload-file-bridge: implement base
1 parent cc76b9f commit 468fc18

3 files changed

Lines changed: 255 additions & 12 deletions

File tree

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
using System;
2+
using System.IO;
3+
using BrowserGuard;
4+
using Xunit;
5+
6+
namespace BrowserGuard.Tests
7+
{
8+
public class FileBridgeTests : IDisposable
9+
{
10+
readonly string tempDir;
11+
12+
public FileBridgeTests()
13+
{
14+
tempDir = Path.Combine(Path.GetTempPath(), "browserguard-bridge-" + Guid.NewGuid().ToString("N"));
15+
Directory.CreateDirectory(tempDir);
16+
}
17+
18+
public void Dispose()
19+
{
20+
try { Directory.Delete(tempDir, true); } catch { }
21+
}
22+
23+
// A fixed day, so a destination naming the day is the one intended here.
24+
static readonly DateTime Now = new(2026, 8, 20, 13, 45, 30);
25+
26+
string Destination => Path.Combine(tempDir, "audit");
27+
28+
UploadFileBridgeConfig Config(string? destination = null) =>
29+
new() { Enabled = true, Destination = destination ?? Destination };
30+
31+
// A file of the user's own, standing in for one being uploaded.
32+
string Source(string name = "report.xlsx", string content = "hello")
33+
{
34+
var path = Path.Combine(tempDir, name);
35+
File.WriteAllText(path, content);
36+
return path;
37+
}
38+
39+
string[] Copies => Directory.Exists(Destination)
40+
? Directory.GetFiles(Destination).Select(Path.GetFileName).OrderBy(name => name).ToArray()!
41+
: [];
42+
43+
[Fact]
44+
public void KeepsACopyOfTheFile()
45+
{
46+
var source = Source(content: "the contents");
47+
48+
var failure = FileBridge.Copy(Config(), source, Now);
49+
50+
Assert.Null(failure);
51+
Assert.Equal("the contents", File.ReadAllText(Path.Combine(Destination, "report.xlsx")));
52+
}
53+
54+
// The destination is on a file server that may have nothing on it yet.
55+
[Fact]
56+
public void MakesTheDestinationWhenItIsNotThere()
57+
{
58+
Assert.False(Directory.Exists(Destination));
59+
60+
FileBridge.Copy(Config(), Source(), Now);
61+
62+
Assert.True(Directory.Exists(Destination));
63+
}
64+
65+
[Fact]
66+
public void ExpandsTheMacrosInTheDestination()
67+
{
68+
var config = Config(Path.Combine(tempDir, "%DATE%", "%PCNAME%"));
69+
70+
var failure = FileBridge.Copy(config, Source(), Now);
71+
72+
Assert.Null(failure);
73+
var expected = Path.Combine(tempDir, "2026-08-20", Environment.MachineName, "report.xlsx");
74+
Assert.True(File.Exists(expected), $"not found: {expected}");
75+
}
76+
77+
// Nothing that was uploaded may be lost to something uploaded later.
78+
[Fact]
79+
public void NumbersACopyRatherThanOverwriteOne()
80+
{
81+
FileBridge.Copy(Config(), Source(content: "first"), Now);
82+
FileBridge.Copy(Config(), Source(content: "second"), Now);
83+
84+
Assert.Equal(["report_2.xlsx", "report.xlsx"], Copies);
85+
Assert.Equal("first", File.ReadAllText(Path.Combine(Destination, "report.xlsx")));
86+
Assert.Equal("second", File.ReadAllText(Path.Combine(Destination, "report_2.xlsx")));
87+
}
88+
89+
[Fact]
90+
public void KeepsNumberingBeyondTheSecond()
91+
{
92+
for (var time = 0; time < 4; time++)
93+
{
94+
FileBridge.Copy(Config(), Source(content: $"copy {time}"), Now);
95+
}
96+
97+
Assert.Equal(
98+
["report_2.xlsx", "report_3.xlsx", "report_4.xlsx", "report.xlsx"],
99+
Copies);
100+
}
101+
102+
// The number goes before the extension, so the copy still opens.
103+
[Fact]
104+
public void KeepsTheExtensionOnANumberedCopy()
105+
{
106+
FileBridge.Copy(Config(), Source("notes.tar.gz"), Now);
107+
FileBridge.Copy(Config(), Source("notes.tar.gz"), Now);
108+
109+
Assert.Equal(["notes.tar_2.gz", "notes.tar.gz"], Copies);
110+
}
111+
112+
[Fact]
113+
public void CopiesAFileWithNoExtension()
114+
{
115+
FileBridge.Copy(Config(), Source("LICENSE"), Now);
116+
FileBridge.Copy(Config(), Source("LICENSE"), Now);
117+
118+
Assert.Equal(["LICENSE", "LICENSE_2"], Copies);
119+
}
120+
121+
// Nothing is copied off the machine unless it was asked for.
122+
[Fact]
123+
public void CopiesNothingWhileDisabled()
124+
{
125+
var config = Config();
126+
config.Enabled = false;
127+
128+
var failure = FileBridge.Copy(config, Source(), Now);
129+
130+
Assert.Null(failure);
131+
Assert.False(Directory.Exists(Destination));
132+
}
133+
134+
[Fact]
135+
public void ReportsThatThereIsNowhereToPutIt()
136+
{
137+
var failure = FileBridge.Copy(Config(""), Source(), Now);
138+
139+
Assert.NotNull(failure);
140+
}
141+
142+
[Fact]
143+
public void ReportsAFileThatIsNotThere()
144+
{
145+
var failure = FileBridge.Copy(Config(), Path.Combine(tempDir, "gone.xlsx"), Now);
146+
147+
Assert.NotNull(failure);
148+
Assert.Contains("gone.xlsx", failure);
149+
}
150+
151+
[Fact]
152+
public void ReportsAPathItCannotUse()
153+
{
154+
var failure = FileBridge.Copy(Config(), "", Now);
155+
156+
Assert.NotNull(failure);
157+
}
158+
}
159+
}

BrowserGuard/FileBridge.cs

Lines changed: 88 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,88 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Linq;
4-
using System.Text;
5-
using System.Threading.Tasks;
6-
7-
namespace BrowserGuard
8-
{
9-
internal class FileBridge
10-
{
11-
}
12-
}
1+
using System;
2+
using System.IO;
3+
4+
namespace BrowserGuard
5+
{
6+
// Keeps a copy of a file the browser uploaded, as evidence of what left the
7+
// machine. The browser only knows the path of the file it sent, so the copy
8+
// is made here.
9+
internal static class FileBridge
10+
{
11+
// Beyond this something is wrong with the destination rather than with
12+
// the name, so it stops rather than counting for ever.
13+
private const int MaxNumbered = 1000;
14+
15+
// null when the copy was made, or when there was nothing to do.
16+
// Otherwise why it could not be made.
17+
internal static string? Copy(
18+
UploadFileBridgeConfig config, string source, DateTime now, Logger? logger = null)
19+
{
20+
if (!config.Enabled)
21+
{
22+
return null;
23+
}
24+
if (string.IsNullOrWhiteSpace(config.Destination))
25+
{
26+
return "no destination is configured";
27+
}
28+
if (string.IsNullOrWhiteSpace(source))
29+
{
30+
return "no file to copy";
31+
}
32+
33+
// Expanded now rather than when the config was read, because a
34+
// destination naming the day changes at midnight.
35+
var destination = PathMacro.Expand(config.Destination, now);
36+
try
37+
{
38+
Directory.CreateDirectory(destination);
39+
}
40+
catch (Exception ex)
41+
{
42+
return $"cannot create {destination}: {ex.Message}";
43+
}
44+
45+
try
46+
{
47+
var copied = CopyWithoutOverwriting(source, destination);
48+
logger?.Log($"UploadFileBridge: copied {source} to {copied}");
49+
return null;
50+
}
51+
catch (Exception ex)
52+
{
53+
return $"cannot copy {source} to {destination}: {ex.Message}";
54+
}
55+
}
56+
57+
// A file that is already there is numbered: "report.xlsx", then
58+
// "report_2.xlsx". The copy refuses to overwrite, so a name taken
59+
// between the check and the copy is simply passed over.
60+
private static string CopyWithoutOverwriting(string source, string destination)
61+
{
62+
var name = Path.GetFileNameWithoutExtension(source);
63+
var extension = Path.GetExtension(source);
64+
65+
for (var number = 1; number <= MaxNumbered; number++)
66+
{
67+
var candidate = Path.Combine(
68+
destination,
69+
number == 1 ? $"{name}{extension}" : $"{name}_{number}{extension}");
70+
if (File.Exists(candidate))
71+
{
72+
continue;
73+
}
74+
try
75+
{
76+
File.Copy(source, candidate, overwrite: false);
77+
return candidate;
78+
}
79+
catch (IOException) when (File.Exists(candidate))
80+
{
81+
// Taken since it was looked at, so the next number is tried.
82+
}
83+
}
84+
throw new IOException(
85+
$"{name}{extension} is already there {MaxNumbered} times over");
86+
}
87+
}
88+
}

BrowserGuard/MessageHandler.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ internal MessageHandler(
7979
var failures = StartupLauncher.Run(config.StartupLauncher, logger);
8080
return new Response { Success = failures is null, Error = failures };
8181
}
82+
// "U " keep a copy of the file the browser is uploading.
83+
else if (message.StartsWith("U "))
84+
{
85+
logger?.Log("Command: bridge upload");
86+
var failure = FileBridge.Copy(
87+
config.UploadFileBridge, message[2..].Trim(), DateTime.Now, logger);
88+
return new Response { Success = failure is null, Error = failure };
89+
}
8290

8391
return new Response { Success = true };
8492
}

0 commit comments

Comments
 (0)