De mensen die mijn techblog volgen weten dat ik de podcasts van Scott Hanselman beluister. Ik ben bij de eerste podcasts begonnen, dus wat ik nu beluister is 9 of 10 jaar oud. Iets wat mij alarmeerde was zijn uitspraak dat SSD's als ze kapot zijn "helemaal niets meer doen". Op zich wel logisch, want als je USB-stick het niet meer doet, dan kun je ook niet bij een klein gedeelte komen. Dat in tegenstelling tot een "harde schijf" met echte draaiende schijven. Er kan iets kapot zijn, maar soms kun je nog wel bij andere onderdelen komen.
In dit artikel van Computer en Techniek uit 2017 staat dat de verwachte levensduur van een SSD schijf ongeveer 5 jaar is (en in een aantal gevallen nog wel langer): link. Maar die levensduur is afhankelijk van hoeveel data je op je harde schijf wegschrijft. Zo was er een tijd geleden een bug van Spotify waarbij deze heel veel data onnodig weg schreef. Dus je hebt niet altijd zelf daar invloed op! Vooral nu we meer thuis werken en misschien ook wel meer op onze eigen computer doen, verkort je dus de levensduur. SSD's zijn geweldig om je computer lekker snel te maken, maar je moet dus wel zorgen dat je de bestanden die je op je computer hebt staan ook back-upt. Dat kun je naar een eigen NAS doen, naar een externe schijf, USB-stick of "naar de cloud".
In april 2020 kwam Computer en Techniek met een mooie actie voor de abonnees, bij Strato kon je een HiDrive abonnement nemen met flinke korting (12 maanden voor een euro per maand). En daarbij heb je 1 TB aan opslagruimte beschikbaar. Dat leek mij een mooie actie, dus ik heb de actie verzilverd. Na dat jaar betaal ik 7.50 euro per maand. Maar ik zie ook dat ik er (al) een jaar niets mee doe. Dat klinkt als weggegooid geld. Tijd om er wat mee te doen. En omdat ik nu ook probeer om elke week een "nuttig" artikel op mijn techblog (en Linked-In) te delen, is dit een mooie case.
Heb je HiDrive en doe je er (nog) niets mee? Dan is dit artikel voor jou. Heb je nog geen HiDrive, maar wil je wel back-ups van je computer(s) en/of website(s)? Dan is het een mogelijke oplossing. Ik zie dat momenteel de actie nog steeds geldt, 1 TB voor 7.50 per maand (link), maar je hebt ook nog een optie voor 250 GB, 500 GB of 3 TB. Strato maakt gebruik van 2 data-centers in Duitsland, dus je data staat niet in een cloud in de Verenigde Staten.
Je hebt een web-interface bij HiDrive, die is bereikbaar op https://my.hidrive.com/ daar zie je ook de linkjes naar apps/applicaties waarmee je back-ups e.d. kunt maken. In de web-omgeving kun je bij je mappen komen en ook alles beheren. Dus in je publieke map zaken aanmaken en in je "user" map zelf je mappenstructuur opzetten en bestanden uploaden, die dan alleen voor jou beschikbaar zijn.
Ik had al een map "pcs" en "websites" aangemaakt om daar back-ups in op te slaan. Maar nu wil ik hier zelf bestanden kunnen uploaden. Je kunt die eerder genoemde standaard apps gebruiken, maar ik ben benieuwd wat je zelf kunt maken. Ik ben eerst naar het developer portal gegaan: https://developer.hidrive.com/ Via de link "Get API Key" kun je een aanvraag indienen om een client_id en secret_id aangeleverd te krijgen. Die krijg ik snel via de mail aangeleverd.
Op deze pagina staan alle stappen om door de autorisatie heen te lopen: link. Je moet eerst inloggen en dan naar de URL gaan met de clientid. Je wordt dan doorgestuurd naar mijn callback-pagina op mijn solution4u.nl-domein. Daar staat in de URL dan de code. Die plak je vervolgens in een POST-request en de output die je daarop ziet, dat is de JSON met een aantal belangrijke gegevens:
{"refresh_token":"xxxxxxxxxxxx","expires_in":3600,"userid":"123456789","access_token":"abcdefghijklmnop","alias":"useralias","token_type":"Bearer"}
Je ziet daar het access_token, die heb je nodig voor al je API-communicatie. Je ziet dat die 3600 seconden geldig is, 1 uur dus. Na dat uur kun je het access_token niet meer gebruiken. Dan moet je met je refresh_token een nieuw access_token aanvragen.
Hierna ben ik in de API documentatie gedoken: link.
Hierna heb ik in een console-applicatie eerst de basis-acties uitgevoerd;
In één map op mijn pc werk ik, daar zet ik foto's en andere bestanden neer waarmee ik aan het werk ben. Die hele map is intussen best wel groot geworden. Daar kan veel weg, maar ook een deel niet. Dus eerst de boel maar syncen naar de cloud, zodat ik in ieder geval een online back-up heb.
De eerste stap die ik doe is de map en alle submappen aanmaken in mijn HiDrive user-omgeving.
Hierna heb ik de bestanden in al deze mappen geüpload, met als voorwaarde dat deze maximaal 25MB groot is. Natuurlijk zouden grotere bestanden ook gesynct moeten worden, maar omdat ik een paar video-dumps heb van onder andere 2 voor 12, Draadstaal en het Mes op Tafel in deze map heb staan die rond de 200MB per stuk zijn (en die niet gebackupt hoeven worden) heb ik eerst deze limiet erin gezet.
Ik heb nu in ieder geval een back-up van mijn systeem in de cloud staan. Zoals ik in het begin al aangaf heb ik een map "pcs" en "websites". Deze heb ik dus in mijn "pcs" map gezet, maar ik wil ook back-ups van mijn websites maken. En daar loop ik tegen een beperking aan. Zoals je ziet heb je een access-token wat geldig is en een refresh-token wat je gebruikt om je verlopen access-token te vernieuwen. Die heb ik nu beschikbaar op mijn pc. Maar als ik straks een plug-in voor wordpress, drupal, joomla en andere websites maak, dan heb ik die gegevens niet. Dus die tokens zouden niet op mijn pc moeten staan, maar ergens online in een veilige omgeving. Die zou geautoriseerde applicaties het access-token kunnen geven en zelfstandig het refresh-token kunnen gebruiken om een nieuw geldig access-token op te vragen en vervolgens weer uit te delen.
Ook daar zou ik zelf iets voor kunnen bouwen, maar naast het feit dat het bouwen van plug-ins voor wordpress, drupal en joomla flink wat tijd zal gaan nemen, heeft Azure hier al een product voor: Azure Key Vault (link). Als ik op de pagina naar de prijzen kijk, dan zie ik daar dat per 10.000 aanvragen je dat 0,026 euro kost. Stel dat ik mijn eigen pc en nog 4 websites heb die elk uur een aanvraag gaan doen. Dan is dat 5 x 24 = 120 aanvragen per dag, in een maand met 31 dagen 3.720 aanvragen per maand. Naast het feit dat ik meer met Azure moet gaan doen om ervaring op te doen met de producten, diensten en code, is dit ook nog eens een goedkoop en veilig product. Ik hoop hier in een volgende blogpost meer informatie over te kunnen geven, omdat ik het dan ook echt zelf gebruik.
Misschien dat ik dan ook met een kant-en-klare oplossing kom zodat ook jij deze uitgebreide functionaliteit van HiDrive kunt gebruiken. Maar gebruik dit blog-bericht eerst maar eens als wake-up call, maak je wel back-ups van je gegevens en zo nee: in de huidige wereld waar iedereen in een phishing-mail kan trappen en vervolgens ransomware al je bestanden, foto's, documenten vergrendelt zodat je er niet meer bij kunt komen: dat wil je niet.
Voor de mensen die zelf aan de slag willen gaan, hieronder de code waarmee ik de "snelle koppeling" gemaakt heb. Het werkt, maar moet natuurlijk wel beter. Eigenlijk wil ik deze applicatie als een soort tray-applicatie uitvoeren en aan kunnen geven welke mappen ik wil syncen (en daar ook weer bestanden uit kunnen sluiten). Een project waar dus nog het nodige aan bijgeschaafd gaat worden.
class Program
{
static void Main(string[] args)
{
if (!File.Exists(DebugFolderLocation))
{
File.WriteAllText(DebugFolderLocation, "");
}
ProcessedFolders = File.ReadAllLines(DebugFolderLocation);
ApiFlow();
}private const string ApiUrl = "https://api.hidrive.strato.com/2.1";
private const string DebugFolderLocation = @"locatie\debug.txt";
private static string[] ProcessedFolders = null;
private const string MyRootFolder = "/users/mijn-online-folderstructuur";private static void ApiFlow()
{
SyncFolderAndSubFolders(@"c:\mijn-map-met-submappen-en-bestanden");
SyncFilesAndSubFolderFiles(@"c:\mijn-map-met-submappen-en-bestanden");
Console.ReadKey();
}private static void SyncFolderAndSubFolders(string folder)
{
var directoryInfo = new DirectoryInfo(folder);
if (!ProcessedFolders.Any(rec => rec == folder))
{
var shortFolder = directoryInfo.FullName.Substring(3).Replace("\\", "/");
CreateFolderIfNotExist(shortFolder);
File.AppendAllLines(DebugFolderLocation, new string[] { folder });
}
foreach (var subfolder in directoryInfo.GetDirectories())
{
SyncFolderAndSubFolders(subfolder.FullName);
}
}private static void SyncFilesAndSubFolderFiles(string folder)
{
var directoryInfo = new DirectoryInfo(folder);
foreach(var fileInfo in directoryInfo.GetFiles())
{
UploadFileIfNotExists(fileInfo);
}foreach (var subfolder in directoryInfo.GetDirectories())
{
SyncFilesAndSubFolderFiles(subfolder.FullName);
}
}private static void CreateFolderIfNotExist(string folderName)
{
string workDirectory = MyRootFolder;
var folderItems = folderName.Split(new char[] { '/' });
foreach (var item in folderItems)
{
var directories = ApiGetCall($"dir?path={workDirectory}");
var directoryModel = JsonSerializer.Deserialize<DirectoryModel>(directories);
if (!directoryModel.Members.Any(rec => rec.Name == item))
{
var path = System.Web.HttpUtility.UrlEncode($"{workDirectory}/{item}");
ApiPostCall("dir", $"path={path}");
Console.WriteLine($"created {workDirectory}/{item}");
}
workDirectory += "/" + item;
}
}private static void UploadFileIfNotExists(FileInfo fileInfo)
{
var fileLength = fileInfo.Length / 1024;
if (fileLength > 25000)
{
Console.WriteLine($"Skip {fileInfo.FullName}");
return;
}var shortFile = fileInfo.FullName.Substring(3).Replace("\\", "/");
string workDirectory = $"{MyRootFolder}/{shortFile}";
var fileName = Path.GetFileName(workDirectory);
var directoryName = workDirectory.Substring(0, workDirectory.Length - fileName.Length);
var path = System.Web.HttpUtility.UrlEncode($"{workDirectory}");
var directoryPath = System.Web.HttpUtility.UrlEncode($"{directoryName.TrimEnd(new char[] { '/' })}");
var doesFileExist = ApiGetCall($"meta?path={path}");
if (doesFileExist == null)
{
Console.WriteLine($"Uploading {fileInfo.FullName}");
var byteList = new List<byte>();
FileStream fileStream = null;
try
{
fileStream = fileInfo.OpenRead();const int readsize = 1024 * 1024;
var canRead = true;
while (canRead)
{
var buffer = new byte[readsize];
var result = fileStream.Read(buffer, 0, readsize);
canRead = (result > 0);
if (canRead)
{
if (result == readsize)
{
byteList.AddRange(buffer);
}
else
{
for (var k = 0; k < result; k++)
{
byteList.Add(buffer[k]);
}
}
}
}
}
finally
{
try
{
fileStream.Close();
}
catch (Exception) { }
}// first, get directory-id:
var directoryData = JsonSerializer.Deserialize<SingleDirectoryModel>(ApiGetCall($"dir?path={directoryPath}&fields=id,name"));
ApiUploadFileCall($"file?dir_id={directoryData.Id}&name={System.Web.HttpUtility.UrlEncode(fileName)}", byteList.ToArray());
}
}
private static string PostCodeOrRefreshTokenToUrl(string code, string refreshtoken = "")
{
var clientId = ConfigurationManager.AppSettings["clientid"];
var clientSecret = ConfigurationManager.AppSettings["clientsecret"];
var url = "https://my.hidrive.com/oauth2/token";
var grantType = string.IsNullOrEmpty(refreshtoken) ? "authorization_code" : "refresh_token";
var postdata = $"client_id={clientId}&client_secret={clientSecret}&grant_type={grantType}&code={code}&refresh_token={refreshtoken}";
var result = "";
HttpWebRequest webRequest = WebRequest.CreateHttp(url);webRequest.Accept = "application/json, text/plain, */*";
webRequest.Headers.Add("Accept-Language", "nl,en-US;q=0.7,en;q=0.3");
webRequest.ContentType = "application/x-www-form-urlencoded";
webRequest.KeepAlive = true;
webRequest.AllowAutoRedirect = true;webRequest.Method = "POST";
var writer = new StreamWriter(webRequest.GetRequestStream());
writer.Write(postdata);
writer.Close();var webResponse = webRequest.GetResponse();
var streamReader = new StreamReader(webResponse.GetResponseStream());
result = streamReader.ReadToEnd();
streamReader.Close();
webResponse.Dispose();
return result;
}private static string ApiGetCall(string detailurl, bool retry = true)
{
WebResponse webResponse = null;
try
{
var accessToken = GetAccessToken();
var url = $"{ApiUrl}/{detailurl}";
var result = "";
HttpWebRequest webRequest = WebRequest.CreateHttp(url);webRequest.Accept = "application/json, text/plain, */*";
webRequest.Headers.Add("Accept-Language", "nl,en-US;q=0.7,en;q=0.3");
webRequest.Headers.Add($"Authorization: Bearer {accessToken}");
webRequest.KeepAlive = true;
webRequest.AllowAutoRedirect = true;webRequest.Method = "GET";
webResponse = webRequest.GetResponse();
var streamReader = new StreamReader(webResponse.GetResponseStream());
result = streamReader.ReadToEnd();
streamReader.Close();
webResponse.Dispose();
return result;
}
catch(WebException we)
{
if (webResponse != null)
{
if (((HttpWebResponse)webResponse).StatusCode == HttpStatusCode.NotFound)
{
return null;
}
}
if (we.Response != null)
{
if (((HttpWebResponse)we.Response).StatusCode == HttpStatusCode.NotFound)
{
return null;
}
}
if (we.Status == WebExceptionStatus.ProtocolError)
{
ReAuthorize();
if (retry)
{
return ApiGetCall(detailurl, false);
}
}
}
catch(Exception x)
{
Console.WriteLine(x.Message);
}
return null;
}private static string ApiPostCall(string detailUrl, string postdata)
{
try
{
var accessToken = GetAccessToken();
var url = $"{ApiUrl}/{detailUrl}";
var result = "";
HttpWebRequest webRequest = WebRequest.CreateHttp(url);webRequest.Accept = "application/json, text/plain, */*";
webRequest.Headers.Add("Accept-Language", "nl,en-US;q=0.7,en;q=0.3");
webRequest.Headers.Add($"Authorization: Bearer {accessToken}");
webRequest.ContentType = "application/x-www-form-urlencoded";
webRequest.KeepAlive = true;
webRequest.AllowAutoRedirect = true;webRequest.Method = "POST";
var writer = new StreamWriter(webRequest.GetRequestStream());
writer.Write(postdata);
writer.Close();var webResponse = webRequest.GetResponse();
var streamReader = new StreamReader(webResponse.GetResponseStream());
result = streamReader.ReadToEnd();
streamReader.Close();
webResponse.Dispose();
return result;
}
catch(Exception x)
{
Console.WriteLine(x.Message);
}
return null;
}private static string ApiUploadFileCall(string detailUrl, byte[] postdata)
{
try
{
var accessToken = GetAccessToken();
var url = $"{ApiUrl}/{detailUrl}";
var result = "";
HttpWebRequest webRequest = WebRequest.CreateHttp(url);webRequest.Accept = "application/json, text/plain, */*";
webRequest.Headers.Add("Accept-Language", "nl,en-US;q=0.7,en;q=0.3");
webRequest.Headers.Add($"Authorization: Bearer {accessToken}");
webRequest.ContentType = "application/octet-stream";
webRequest.KeepAlive = true;
webRequest.AllowAutoRedirect = true;webRequest.Method = "POST";
var binaryWriter = new BinaryWriter(webRequest.GetRequestStream());
binaryWriter.Write(postdata);
binaryWriter.Close();var webResponse = webRequest.GetResponse();
var streamReader = new StreamReader(webResponse.GetResponseStream());
result = streamReader.ReadToEnd();
streamReader.Close();
webResponse.Dispose();
return result;
}
catch (Exception x)
{
Console.WriteLine(x.Message);
}
return null;
}private static void ReAuthorize()
{
var tokenData = PostCodeOrRefreshTokenToUrl("", GetRefreshToken());
var tokenObject = JsonSerializer.Deserialize<TokenData>(tokenData);
SaveAccessToken(tokenObject.AccessToken);
SaveRefreshToken(tokenObject.RefreshToken);
}private static string GetAccessToken()
{
return File.ReadAllText(@"bestandslocatie\accesstoken.txt");
}
private static void SaveAccessToken(string accessToken)
{
File.WriteAllText(@"bestandslocatie\accesstoken.txt", accessToken);
}private static string GetRefreshToken()
{
return File.ReadAllText(@"bestandslocatie\refreshtoken.txt");
}private static void SaveRefreshToken(string refreshToken)
{
File.WriteAllText(@"bestandslocatie\refreshtoken.txt", refreshToken);
}
}
Voor de oplettende code-lezer, je ziet een aantal Modellen die gebruikt worden (voor het resultaat). Die voeg ik hier nog even voor de volledigheid toe:
using System.Text.Json.Serialization;
namespace hidrive.Models
{
public class DirectoryMember
{
[JsonPropertyName("name")]
public string Name { get; set; }
}
public class DirectoryModel
{
[JsonPropertyName("path")]
public string Path { get;set;}
[JsonPropertyName("members")]
public DirectoryMember[] Members { get; set; }
}
public class SingleDirectoryModel
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("id")]
public string Id { get; set; }
}
public class TokenData
{
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
[JsonPropertyName("expires_in")]
public int ExpiresIn { get; set; }
[JsonPropertyName("userid")]
public string UserId { get; set; }
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
[JsonPropertyName("alias")]
public string Alias { get; set; }
[JsonPropertyName("token_type")]
public string TokenType { get; set; }
}
}