File Upload
The Editor .NET libraries provide an Upload
class that can be used with the upload
and uploadMany
field types to provide end users with the ability to upload files to a server. The goal of the Upload
class is to make the server-side aspect of file uploading as simple as possible, while retaining the same flexibility that you would expect from the Editor libraries.
The documentation here refers to the .NET aspect of the upload field type. Please refer to the client-side documentation for details on how to configure the Javascript aspect of the file upload options in Editor.
Overview
The Upload
class provides the following features:
- Save files to local file system or database
- Store information about each file in a database table - what information is stored is configurable
- File saving override options for custom actions (for example creating thumbnails)
- Easy validation of file extensions
- Custom validation for more complex options
In a typical situation it is expected that you will store the file on the server's file system (not the database), although that is possible if you require) and meta information about the file on the database so it can be accessed through the file()
method.
Consider for example the following two tables:
Table: staff Table: image
+----+---------+-------+-------+ +----+-----------------+----------+
| id | name | title | image | | id | fileName | fileSize |
+----+---------+-------+-------+ +----+-----------------+----------+
| 1 | Allan | CEO | 1 | | 1 | Allan.png | 4532 |
| 2 | Charlie | CTO | 4 | | 4 | Charlie-CTO.png | 9408 |
| 3 | Fred | CFO | 9 | | 9 | Photo.png | 2054 |
+----+---------+-------+-------+ +----+-----------------+----------+
Here we have a staff
table where each row has a reference to the image
table. The Editor initialisation to read the data from the staff
table might look like the following:
Controller:
public IHttpActionResult Staff()
{
var request = HttpContext.Current.Request;
var settings = Properties.Settings.Default;
using (var db = new Database(settings.DbType, settings.DbConnection))
{
var response = new Editor(db, "staff")
.Model<StaffModel>()
.Process(request)
.Data();
return Json(response);
}
}
Model:
public class StaffModel
{
public string name { get; set; }
public string title { get; set; }
public int image { get; set; }
}
This gives us the information required on the client-side to show staff information, but not yet to upload files and display information about files.
The documentation below will build upon this basic example to add the ability to upload a new file in the Editor interface. Note that although this example focuses on images, any file type can be used - what files you use will be dependent upon your own requirements. Additionally the database structure shown above is not fixed - for example you can use a link table if you require by adding an additional left join.
The Upload class
The new Upload
instance is attached to the field that should have its value edited when a new image is selected - in the example discussed here that is the image
column. This is done using the Field.Upload()
method:
new Field( "image" )
.Upload( new Upload( ... ) )
.SetFormatter( Format.NullEmpty() );
The Upload
class provides the ability to make use of chaining, so it is easy to construct a full CRUD application, including conditional uploads in a few lines of code.
Note that we also use a set formatter, the Format.NullEmpty
formatter in this case, which will cause Editor to write a null
value to the database when no image value is submitted from the client-side. This is optional, and will depend upon the exact schema used in your database, but this is typically desirable.
One-to-many support
The uploadMany
field type provides the ability to assign multiple files to a single field in an Editor form - i.e. one-to-many. Editor's one-to-many support is provided through the MJoin
class (see the MJoin documentation).
The following documentation will focus primarily on using the upload
type with fields in the main Editor instance, but the Upload
class can be attached to fields defined in an MJoin
instance in exactly the same was as for fields in the main Editor instance to provide the data required for the uploadMany
field type.
File location
The Upload
constructor accepts a single, optional, parameter - a string with the full system path where the uploaded file should be stored. Note that it is important you use the full path rather than a relative path to reduce complexities of include files! Use the request.PhysicalApplicationPath
option to get the path to the root directory of your application if required.
The path given can have three special strings in it that the Upload
class will replace so the file can be easily referenced from a database and to remove the chance of a file name collision:
__ID__
- The primary key value of the row that refers to this file__EXTN__
- The file extension (including the dot)__NAME__
- The full file name (including the extension).
These parameters are simply inserted into the string where you would like them to appear:
new Field( "image" )
.Upload( new Upload( request.PhysicalApplicationPath + @"uploads\__ID____EXTN__" ) )
.SetFormatter( Format.NullEmpty() );
If you wish to perform more complex actions for storing files, such using a directory structure to limit the number of files in each directory to something safe for your operating system if you are working with large numbers of files, please see the Custom upload actions section below.
If no value is given in the constructor for the file path the Upload
class will not attempt to write the uploaded file to the file system unless Upload.Action()
is used (which operates in exactly the same way as the constructor). This can be useful if you are storing the file's binary data in the database.
Database information
For efficiency you will typically wish to store information about each file in a database table. In combination with the file()
method, as discussed above, this makes it very easy to get information about each file such as the file's path to load it in a web-browser, the file type and even if the file exists!
The Upload
class provides a Db()
method that is used for this purpose. It takes up to four parameters:
string
- The database table name where the file information should be storedstring
- The database primary key column name. TheUpload
class requires that the database table have a single primary key so each row can be uniquely identified.Dictionary<string, object>
- A list of the columns to be written to on upload. The array key's are the database column names and the values can be one of:- A
Upload.DbType
enum - describes information that should be obtained from the file - see the table below for a full list of the options available - A value (e.g a string, number, etc) which is written directly to the database
- An delegate which is executed and the return value written to the database. The delegate function takes a single parameter - the
HttpPostedFile
object for the uploaded file.
- A
Action<Dictionary<string, object>>
(optional) - A formatting function for each row read from the database. A single parameter is passed in, the object for the file from the database. It expects no return value - any manipulation of the object is propagated back.
The constants that can be used for the values are:
Constant | Meaning |
---|---|
Upload.DbType.Content |
File content |
Upload.DbType.ContentType |
Content type |
Upload.DbType.Extn |
File extension (with the dot) |
Upload.DbType.FileName |
File name (with extension) |
Upload.DbType.FileSize |
File size (bytes) |
Upload.DbType.MimeType |
MIME type |
Upload.DbType.ReadOnly |
Editor will not write a value to this field, just read its value which might come from a default, trigger or be updated from some other location. Since 1.6.2 |
Upload.DbType.SystemPath |
Full system path to the file |
Upload.DbType.WebPath |
HTTP path to the file. This is derived from the system path by removing PhysicalApplicationPath . If your images are stored outside of the document root a custom value would need to be used. |
The db()
method is used for both writing information to the database, and also automatically populating the data for the file()
method. The parameters defined here are also available on the client-side (with the exception of the Content
option).
Continuing the illustrative example we now have:
new Field( "image" )
.Upload(
new Upload( request.PhysicalApplicationPath + @"uploads\__ID____EXTN__" )
.Db("image", "id", new Dictionary<string, object>
{
{"fileName", Upload.DbType.FileName},
{"fileSize", Upload.DbType.FileSize}
})
)
.SetFormatter( Format.NullEmpty() );
Validation
It is important that the files uploaded are validated to ensure that they are of an expected type (you don't want a Word document uploaded where your software expects an image for example!). There are two validation methods provided by the Upload
class:
- Simple extension validation
- Custom validation methods
Provided validation functions
The Validation
class provides two built in validation methods that can be used to validate file uploads. The function signatures for the file validations differ from the field validators in that they do not accept a Validate.Options
parameter - instead they take parameters which are defined by the validator, including the error message to be returned.
The built in validators are:
fileExtensions( string[] valid, string errorMessage )
- Check that the uploaded file's extension is one of those provided in thevalid
list. This check is case-insensitive, sopng
in the list would accept.PNG
,.pnG
, etc, as valid extensions). Also, it is worth noting that you should not rely upon extension validation only for secure validation as it is trivial to modify a file name extension. It is however a useful and simple sanity check.fileSize( int size, string errorMessage )
- Check that the uploaded file is equal to or below a given size (in bytes).
As an example, to check that only image files are uploaded you might use:
new Upload( ... )
.Validator(
Validation.fileExtensions(
new [] {"png","jpg","gif"},
'Only image files can be uploaded (png, jpg and gif)'
)
)
Custom validation
A custom validation method can perform any check you wish, for example actually going into the file's binary content to ensure that it is of the type expected, limiting file size, etc. This is done through use of a closure method that is attached using the Upload.Validator()
method - a single parameter is required, the closure validation method.
The closure function, when executed will be given a single method, the HttpPostedFile
instance for the uploaded file. The return value should be null
if validation passes and a string containing an error message if it fails.
Consider the following example
new Upload( ... )
.Validator( file => {
if ( file.ContentLength >= 500000 ) {
return "Files must be smaller than 500K";
}
return null;
} )
Multiple validation methods can be used for a single Upload
instance by simply calling Upload.Validator()
multiple times. The validators are executed in sequence when a file is uploaded.
Custom upload actions
There may be occasions when you find that the simple file move options provided by the Upload
class constructor aren't enough for your application. This might be because you want to create thumbnails from an uploaded image so they are immediately available for use, or to use a complex directory structure for the uploaded files. Whatever the reason the Upload
class provides the option of specifying a closure function that will be executed in place of the detail action.
As with the simple file path the closure method is specified using the constructor or the Upload.Action()
method. When executed it will receive two parameters:
- The
HttpPostedFile
instance for the uploaded file. - The primary key value for the file if
Upload.Db()
was used to store information in the database (otherwise this value will benull
).
The return value is the value given to the client-side to identify the file. Typically this will be the primary key value, but you could also use a file path or any other information.
Consider the following example:
new Field( "image" )
.Upload(
new Upload( (file, id) => {
file.SaveAs( request.PhysicalApplicationPath + @"uploads\" + id );
} )
.Db("image", "id", new Dictionary<string, object>
{
{"fileName", Upload.DbType.FileName},
{"fileSize", Upload.DbType.FileSize}
})
)
.SetFormatter( Format.NullEmpty() );
In this case we are simply storing the file using HttpPostedFile.SaveAs()
, but much more complex actions can be taken if required.
Deletion of orphaned files
Providing users the ability to upload files to a server also requires the ability to remove files - although users only have this ability indirectly (by deleting the data that has a reference to the file). The Upload
class has a DbClean()
method available that gives you this ability.
The method takes either one or two arguments:
- A table and field name string (in
table.field
format) to tell Editor where to look for files that are in use. This parameter is optional for a standard field, but is required when used inside anMjoin
field. - A callback function that is executed whenever Editor performs a database clean (which is on create, edit and remove - not upload). It is passed a single argument (of type
List<Dictionary<string, object>>
); a list of information from the database about rows which are no longer referenced by the external table (note this only checks the host Editor table, not any other database tables - so if you have multiple tables using the same files table, you will need to execute your own queries). The information provided in the given list of dictionaries is defined by the options specified for theUpload.Db()
method.
The callback function can return true
to indicate that the rows should be removed from the database. false
or null
will result in no action being performed by the libraries.
The callback function can therefore be used to delete files from the file system, or potentially to execute your own queries on the database to ensure there are no unwanted orphaned records.
new Field( "image" )
.Upload(
new Upload( (file, id) => {
file.SaveAs( request.PhysicalApplicationPath + @"uploads\" + id );
return id;
} )
.Db("image", "id", new Dictionary<string, object>
{
{"fileName", Upload.DbType.FileName},
{"webPath", Upload.DbType.WebPath},
{"systemPath", Upload.DbType.SystemPath}
})
.DbClean(data =>
{
foreach (var row in data)
{
File.Delete(row["system_path"].ToString());
}
return true;
})
);
Events
Events can also be used to handle the deletion of files that are no longer referenced. The preRemove
event for example can be used to remove files immediately prior to a row being deleted.
.NET API documentation
The .NET API developer documentation for the Editor .NET classes is available for detailed and technical discussion about the methods and classes discussed above.